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 my $min_sdate = 999999999999;
2552 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2553 next unless $cust_bill_pkg->pkgnum > 0;
2554 $min_sdate = $cust_bill_pkg->sdate if $cust_bill_pkg->sdate < $min_sdate;
2555 $max_edate = $cust_bill_pkg->edate if $cust_bill_pkg->edate > $max_edate;
2558 $invoice_data{'bill_period'} = '';
2559 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2560 . " to " . time2str('%e %h', $max_edate)
2561 if ($max_edate != 0 && $min_sdate != 999999999999);
2563 $invoice_data{finance_section} = '';
2564 if ( $conf->config('finance_pkgclass') ) {
2566 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2567 $invoice_data{finance_section} = $pkg_class->categoryname;
2569 $invoice_data{finance_amount} = '0.00';
2570 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2572 my $countrydefault = $conf->config('countrydefault') || 'US';
2573 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2574 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2575 my $method = $prefix.$_;
2576 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2578 $invoice_data{'ship_country'} = ''
2579 if ( $invoice_data{'ship_country'} eq $countrydefault );
2581 $invoice_data{'cid'} = $params{'cid'}
2584 if ( $cust_main->country eq $countrydefault ) {
2585 $invoice_data{'country'} = '';
2587 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2591 $invoice_data{'address'} = \@address;
2593 $cust_main->payname.
2594 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2595 ? " (P.O. #". $cust_main->payinfo. ")"
2599 push @address, $cust_main->company
2600 if $cust_main->company;
2601 push @address, $cust_main->address1;
2602 push @address, $cust_main->address2
2603 if $cust_main->address2;
2605 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2606 push @address, $invoice_data{'country'}
2607 if $invoice_data{'country'};
2609 while (scalar(@address) < 5);
2611 $invoice_data{'logo_file'} = $params{'logo_file'}
2612 if $params{'logo_file'};
2613 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2614 if $params{'barcode_file'};
2615 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2616 if $params{'barcode_img'};
2617 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2618 if $params{'barcode_cid'};
2620 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2621 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2622 #my $balance_due = $self->owed + $pr_total - $cr_total;
2623 my $balance_due = $self->owed + $pr_total;
2624 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2625 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2626 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2627 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2629 my $summarypage = '';
2630 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2633 $invoice_data{'summarypage'} = $summarypage;
2635 warn "$me substituting variables in notes, footer, smallfooter\n"
2638 foreach my $include (qw( notes footer smallfooter coupon )) {
2640 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2643 if ( $conf->exists($inc_file, $agentnum)
2644 && length( $conf->config($inc_file, $agentnum) ) ) {
2646 @inc_src = $conf->config($inc_file, $agentnum);
2650 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2652 my $convert_map = $convert_maps{$format}{$include};
2654 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2655 s/--\@\]/$delimiters{$format}[1]/g;
2658 &$convert_map( $conf->config($inc_file, $agentnum) );
2662 my $inc_tt = new Text::Template (
2664 SOURCE => [ map "$_\n", @inc_src ],
2665 DELIMITERS => $delimiters{$format},
2666 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2668 unless ( $inc_tt->compile() ) {
2669 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2670 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2674 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2676 $invoice_data{$include} =~ s/\n+$//
2677 if ($format eq 'latex');
2680 $invoice_data{'po_line'} =
2681 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2682 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2685 my %money_chars = ( 'latex' => '',
2686 'html' => $conf->config('money_char') || '$',
2689 my $money_char = $money_chars{$format};
2691 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2692 'html' => $conf->config('money_char') || '$',
2695 my $other_money_char = $other_money_chars{$format};
2696 $invoice_data{'dollar'} = $other_money_char;
2698 my @detail_items = ();
2699 my @total_items = ();
2703 $invoice_data{'detail_items'} = \@detail_items;
2704 $invoice_data{'total_items'} = \@total_items;
2705 $invoice_data{'buf'} = \@buf;
2706 $invoice_data{'sections'} = \@sections;
2708 warn "$me generating sections\n"
2711 my $previous_section = { 'description' => 'Previous Charges',
2712 'subtotal' => $other_money_char.
2713 sprintf('%.2f', $pr_total),
2714 'summarized' => $summarypage ? 'Y' : '',
2716 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2717 join(' / ', map { $cust_main->balance_date_range(@$_) }
2718 $self->_prior_month30s
2720 if $conf->exists('invoice_include_aging');
2723 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2724 'subtotal' => $taxtotal, # adjusted below
2725 'summarized' => $summarypage ? 'Y' : '',
2727 my $tax_weight = _pkg_category($tax_section->{description})
2728 ? _pkg_category($tax_section->{description})->weight
2730 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2731 $tax_section->{'sort_weight'} = $tax_weight;
2734 my $adjusttotal = 0;
2735 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2736 'subtotal' => 0, # adjusted below
2737 'summarized' => $summarypage ? 'Y' : '',
2739 my $adjust_weight = _pkg_category($adjust_section->{description})
2740 ? _pkg_category($adjust_section->{description})->weight
2742 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2743 $adjust_section->{'sort_weight'} = $adjust_weight;
2745 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2746 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2747 $invoice_data{'multisection'} = $multisection;
2748 my $late_sections = [];
2749 my $extra_sections = [];
2750 my $extra_lines = ();
2751 if ( $multisection ) {
2752 ($extra_sections, $extra_lines) =
2753 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2754 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2756 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2758 push @detail_items, @$extra_lines if $extra_lines;
2760 $self->_items_sections( $late_sections, # this could stand a refactor
2762 $escape_function_nonbsp,
2766 if ($conf->exists('svc_phone_sections')) {
2767 my ($phone_sections, $phone_lines) =
2768 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2769 push @{$late_sections}, @$phone_sections;
2770 push @detail_items, @$phone_lines;
2773 push @sections, { 'description' => '', 'subtotal' => '' };
2776 unless ( $conf->exists('disable_previous_balance')
2777 || $conf->exists('previous_balance-summary_only')
2781 warn "$me adding previous balances\n"
2784 foreach my $line_item ( $self->_items_previous ) {
2787 ext_description => [],
2789 $detail->{'ref'} = $line_item->{'pkgnum'};
2790 $detail->{'quantity'} = 1;
2791 $detail->{'section'} = $previous_section;
2792 $detail->{'description'} = &$escape_function($line_item->{'description'});
2793 if ( exists $line_item->{'ext_description'} ) {
2794 @{$detail->{'ext_description'}} = map {
2795 &$escape_function($_);
2796 } @{$line_item->{'ext_description'}};
2798 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2799 $line_item->{'amount'};
2800 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2802 push @detail_items, $detail;
2803 push @buf, [ $detail->{'description'},
2804 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2810 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2811 push @buf, ['','-----------'];
2812 push @buf, [ 'Total Previous Balance',
2813 $money_char. sprintf("%10.2f", $pr_total) ];
2817 if ( $conf->exists('svc_phone-did-summary') ) {
2818 warn "$me adding DID summary\n"
2821 my ($didsummary,$minutes) = $self->_did_summary;
2822 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2824 { 'description' => $didsummary_desc,
2825 'ext_description' => [ $didsummary, $minutes ],
2830 foreach my $section (@sections, @$late_sections) {
2832 warn "$me adding section \n". Dumper($section)
2835 # begin some normalization
2836 $section->{'subtotal'} = $section->{'amount'}
2838 && !exists($section->{subtotal})
2839 && exists($section->{amount});
2841 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2842 if ( $invoice_data{finance_section} &&
2843 $section->{'description'} eq $invoice_data{finance_section} );
2845 $section->{'subtotal'} = $other_money_char.
2846 sprintf('%.2f', $section->{'subtotal'})
2849 # continue some normalization
2850 $section->{'amount'} = $section->{'subtotal'}
2854 if ( $section->{'description'} ) {
2855 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2860 warn "$me setting options\n"
2863 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2865 $options{'section'} = $section if $multisection;
2866 $options{'format'} = $format;
2867 $options{'escape_function'} = $escape_function;
2868 $options{'format_function'} = sub { () } unless $unsquelched;
2869 $options{'unsquelched'} = $unsquelched;
2870 $options{'summary_page'} = $summarypage;
2871 $options{'skip_usage'} =
2872 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2873 $options{'multilocation'} = $multilocation;
2874 $options{'multisection'} = $multisection;
2876 warn "$me searching for line items\n"
2879 foreach my $line_item ( $self->_items_pkg(%options) ) {
2881 warn "$me adding line item $line_item\n"
2885 ext_description => [],
2887 $detail->{'ref'} = $line_item->{'pkgnum'};
2888 $detail->{'quantity'} = $line_item->{'quantity'};
2889 $detail->{'section'} = $section;
2890 $detail->{'description'} = &$escape_function($line_item->{'description'});
2891 if ( exists $line_item->{'ext_description'} ) {
2892 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2894 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2895 $line_item->{'amount'};
2896 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2897 $line_item->{'unit_amount'};
2898 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2900 push @detail_items, $detail;
2901 push @buf, ( [ $detail->{'description'},
2902 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2904 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2908 if ( $section->{'description'} ) {
2909 push @buf, ( ['','-----------'],
2910 [ $section->{'description'}. ' sub-total',
2911 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2920 $invoice_data{current_less_finance} =
2921 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2923 if ( $multisection && !$conf->exists('disable_previous_balance')
2924 || $conf->exists('previous_balance-summary_only') )
2926 unshift @sections, $previous_section if $pr_total;
2929 warn "$me adding taxes\n"
2932 foreach my $tax ( $self->_items_tax ) {
2934 $taxtotal += $tax->{'amount'};
2936 my $description = &$escape_function( $tax->{'description'} );
2937 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2939 if ( $multisection ) {
2941 my $money = $old_latex ? '' : $money_char;
2942 push @detail_items, {
2943 ext_description => [],
2946 description => $description,
2947 amount => $money. $amount,
2949 section => $tax_section,
2954 push @total_items, {
2955 'total_item' => $description,
2956 'total_amount' => $other_money_char. $amount,
2961 push @buf,[ $description,
2962 $money_char. $amount,
2969 $total->{'total_item'} = 'Sub-total';
2970 $total->{'total_amount'} =
2971 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2973 if ( $multisection ) {
2974 $tax_section->{'subtotal'} = $other_money_char.
2975 sprintf('%.2f', $taxtotal);
2976 $tax_section->{'pretotal'} = 'New charges sub-total '.
2977 $total->{'total_amount'};
2978 push @sections, $tax_section if $taxtotal;
2980 unshift @total_items, $total;
2983 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2985 push @buf,['','-----------'];
2986 push @buf,[( $conf->exists('disable_previous_balance')
2988 : 'Total New Charges'
2990 $money_char. sprintf("%10.2f",$self->charged) ];
2996 $item = $conf->config('previous_balance-exclude_from_total')
2997 || 'Total New Charges'
2998 if $conf->exists('previous_balance-exclude_from_total');
2999 my $amount = $self->charged +
3000 ( $conf->exists('disable_previous_balance') ||
3001 $conf->exists('previous_balance-exclude_from_total')
3005 $total->{'total_item'} = &$embolden_function($item);
3006 $total->{'total_amount'} =
3007 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3008 if ( $multisection ) {
3009 if ( $adjust_section->{'sort_weight'} ) {
3010 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3011 sprintf("%.2f", ($self->billing_balance || 0) );
3013 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3014 sprintf('%.2f', $self->charged );
3017 push @total_items, $total;
3019 push @buf,['','-----------'];
3022 sprintf( '%10.2f', $amount )
3027 unless ( $conf->exists('disable_previous_balance') ) {
3028 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3031 my $credittotal = 0;
3032 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3035 $total->{'total_item'} = &$escape_function($credit->{'description'});
3036 $credittotal += $credit->{'amount'};
3037 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3038 $adjusttotal += $credit->{'amount'};
3039 if ( $multisection ) {
3040 my $money = $old_latex ? '' : $money_char;
3041 push @detail_items, {
3042 ext_description => [],
3045 description => &$escape_function($credit->{'description'}),
3046 amount => $money. $credit->{'amount'},
3048 section => $adjust_section,
3051 push @total_items, $total;
3055 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3058 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3059 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3063 my $paymenttotal = 0;
3064 foreach my $payment ( $self->_items_payments ) {
3066 $total->{'total_item'} = &$escape_function($payment->{'description'});
3067 $paymenttotal += $payment->{'amount'};
3068 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3069 $adjusttotal += $payment->{'amount'};
3070 if ( $multisection ) {
3071 my $money = $old_latex ? '' : $money_char;
3072 push @detail_items, {
3073 ext_description => [],
3076 description => &$escape_function($payment->{'description'}),
3077 amount => $money. $payment->{'amount'},
3079 section => $adjust_section,
3082 push @total_items, $total;
3084 push @buf, [ $payment->{'description'},
3085 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3088 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3090 if ( $multisection ) {
3091 $adjust_section->{'subtotal'} = $other_money_char.
3092 sprintf('%.2f', $adjusttotal);
3093 push @sections, $adjust_section
3094 unless $adjust_section->{sort_weight};
3099 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3100 $total->{'total_amount'} =
3101 &$embolden_function(
3102 $other_money_char. sprintf('%.2f', $summarypage
3104 $self->billing_balance
3105 : $self->owed + $pr_total
3108 if ( $multisection && !$adjust_section->{sort_weight} ) {
3109 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3110 $total->{'total_amount'};
3112 push @total_items, $total;
3114 push @buf,['','-----------'];
3115 push @buf,[$self->balance_due_msg, $money_char.
3116 sprintf("%10.2f", $balance_due ) ];
3120 if ( $multisection ) {
3121 if ($conf->exists('svc_phone_sections')) {
3123 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3124 $total->{'total_amount'} =
3125 &$embolden_function(
3126 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3128 my $last_section = pop @sections;
3129 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3130 $total->{'total_amount'};
3131 push @sections, $last_section;
3133 push @sections, @$late_sections
3137 my @includelist = ();
3138 push @includelist, 'summary' if $summarypage;
3139 foreach my $include ( @includelist ) {
3141 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3144 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3146 @inc_src = $conf->config($inc_file, $agentnum);
3150 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3152 my $convert_map = $convert_maps{$format}{$include};
3154 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3155 s/--\@\]/$delimiters{$format}[1]/g;
3158 &$convert_map( $conf->config($inc_file, $agentnum) );
3162 my $inc_tt = new Text::Template (
3164 SOURCE => [ map "$_\n", @inc_src ],
3165 DELIMITERS => $delimiters{$format},
3166 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3168 unless ( $inc_tt->compile() ) {
3169 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3170 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3174 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3176 $invoice_data{$include} =~ s/\n+$//
3177 if ($format eq 'latex');
3182 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3183 /invoice_lines\((\d*)\)/;
3184 $invoice_lines += $1 || scalar(@buf);
3187 die "no invoice_lines() functions in template?"
3188 if ( $format eq 'template' && !$wasfunc );
3190 if ($format eq 'template') {
3192 if ( $invoice_lines ) {
3193 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3194 $invoice_data{'total_pages'}++
3195 if scalar(@buf) % $invoice_lines;
3198 #setup subroutine for the template
3199 sub FS::cust_bill::_template::invoice_lines {
3200 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3202 scalar(@FS::cust_bill::_template::buf)
3203 ? shift @FS::cust_bill::_template::buf
3212 push @collect, split("\n",
3213 $text_template->fill_in( HASH => \%invoice_data,
3214 PACKAGE => 'FS::cust_bill::_template'
3217 $FS::cust_bill::_template::page++;
3219 map "$_\n", @collect;
3221 warn "filling in template for invoice ". $self->invnum. "\n"
3223 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3226 $text_template->fill_in(HASH => \%invoice_data);
3230 # helper routine for generating date ranges
3231 sub _prior_month30s {
3234 [ 1, 2592000 ], # 0-30 days ago
3235 [ 2592000, 5184000 ], # 30-60 days ago
3236 [ 5184000, 7776000 ], # 60-90 days ago
3237 [ 7776000, 0 ], # 90+ days ago
3240 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3241 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3246 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3248 Returns an postscript invoice, as a scalar.
3250 Options can be passed as a hashref (recommended) or as a list of time, template
3251 and then any key/value pairs for any other options.
3253 I<time> an optional value used to control the printing of overdue messages. The
3254 default is now. It isn't the date of the invoice; that's the `_date' field.
3255 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3256 L<Time::Local> and L<Date::Parse> for conversion functions.
3258 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3265 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3266 my $ps = generate_ps($file);
3268 unlink($barcodefile);
3273 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3275 Returns an PDF invoice, as a scalar.
3277 Options can be passed as a hashref (recommended) or as a list of time, template
3278 and then any key/value pairs for any other options.
3280 I<time> an optional value used to control the printing of overdue messages. The
3281 default is now. It isn't the date of the invoice; that's the `_date' field.
3282 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3283 L<Time::Local> and L<Date::Parse> for conversion functions.
3285 I<template>, if specified, is the name of a suffix for alternate invoices.
3287 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3294 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3295 my $pdf = generate_pdf($file);
3297 unlink($barcodefile);
3302 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3304 Returns an HTML invoice, as a scalar.
3306 I<time> an optional value used to control the printing of overdue messages. The
3307 default is now. It isn't the date of the invoice; that's the `_date' field.
3308 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3309 L<Time::Local> and L<Date::Parse> for conversion functions.
3311 I<template>, if specified, is the name of a suffix for alternate invoices.
3313 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3315 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3316 when emailing the invoice as part of a multipart/related MIME email.
3324 %params = %{ shift() };
3326 $params{'time'} = shift;
3327 $params{'template'} = shift;
3328 $params{'cid'} = shift;
3331 $params{'format'} = 'html';
3333 $self->print_generic( %params );
3336 # quick subroutine for print_latex
3338 # There are ten characters that LaTeX treats as special characters, which
3339 # means that they do not simply typeset themselves:
3340 # # $ % & ~ _ ^ \ { }
3342 # TeX ignores blanks following an escaped character; if you want a blank (as
3343 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3347 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3348 $value =~ s/([<>])/\$$1\$/g;
3354 encode_entities($value);
3358 sub _html_escape_nbsp {
3359 my $value = _html_escape(shift);
3360 $value =~ s/ +/ /g;
3364 #utility methods for print_*
3366 sub _translate_old_latex_format {
3367 warn "_translate_old_latex_format called\n"
3374 if ( $line =~ /^%%Detail\s*$/ ) {
3376 push @template, q![@--!,
3377 q! foreach my $_tr_line (@detail_items) {!,
3378 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3379 q! $_tr_line->{'description'} .= !,
3380 q! "\\tabularnewline\n~~".!,
3381 q! join( "\\tabularnewline\n~~",!,
3382 q! @{$_tr_line->{'ext_description'}}!,
3386 while ( ( my $line_item_line = shift )
3387 !~ /^%%EndDetail\s*$/ ) {
3388 $line_item_line =~ s/'/\\'/g; # nice LTS
3389 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3390 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3391 push @template, " \$OUT .= '$line_item_line';";
3394 push @template, '}',
3397 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3399 push @template, '[@--',
3400 ' foreach my $_tr_line (@total_items) {';
3402 while ( ( my $total_item_line = shift )
3403 !~ /^%%EndTotalDetails\s*$/ ) {
3404 $total_item_line =~ s/'/\\'/g; # nice LTS
3405 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3406 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3407 push @template, " \$OUT .= '$total_item_line';";
3410 push @template, '}',
3414 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3415 push @template, $line;
3421 warn "$_\n" foreach @template;
3430 #check for an invoice-specific override
3431 return $self->invoice_terms if $self->invoice_terms;
3433 #check for a customer- specific override
3434 my $cust_main = $self->cust_main;
3435 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3437 #use configured default
3438 $conf->config('invoice_default_terms') || '';
3444 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3445 $duedate = $self->_date() + ( $1 * 86400 );
3452 $self->due_date ? time2str(shift, $self->due_date) : '';
3455 sub balance_due_msg {
3457 my $msg = 'Balance Due';
3458 return $msg unless $self->terms;
3459 if ( $self->due_date ) {
3460 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3461 } elsif ( $self->terms ) {
3462 $msg .= ' - '. $self->terms;
3467 sub balance_due_date {
3470 if ( $conf->exists('invoice_default_terms')
3471 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3472 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3477 =item invnum_date_pretty
3479 Returns a string with the invoice number and date, for example:
3480 "Invoice #54 (3/20/2008)"
3484 sub invnum_date_pretty {
3486 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3491 Returns a string with the date, for example: "3/20/2008"
3497 time2str($date_format, $self->_date);
3500 use vars qw(%pkg_category_cache);
3501 sub _items_sections {
3504 my $summarypage = shift;
3506 my $extra_sections = shift;
3510 my %late_subtotal = ();
3513 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3516 my $usage = $cust_bill_pkg->usage;
3518 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3519 next if ( $display->summary && $summarypage );
3521 my $section = $display->section;
3522 my $type = $display->type;
3524 $not_tax{$section} = 1
3525 unless $cust_bill_pkg->pkgnum == 0;
3527 if ( $display->post_total && !$summarypage ) {
3528 if (! $type || $type eq 'S') {
3529 $late_subtotal{$section} += $cust_bill_pkg->setup
3530 if $cust_bill_pkg->setup != 0;
3534 $late_subtotal{$section} += $cust_bill_pkg->recur
3535 if $cust_bill_pkg->recur != 0;
3538 if ($type && $type eq 'R') {
3539 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3540 if $cust_bill_pkg->recur != 0;
3543 if ($type && $type eq 'U') {
3544 $late_subtotal{$section} += $usage
3545 unless scalar(@$extra_sections);
3550 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3552 if (! $type || $type eq 'S') {
3553 $subtotal{$section} += $cust_bill_pkg->setup
3554 if $cust_bill_pkg->setup != 0;
3558 $subtotal{$section} += $cust_bill_pkg->recur
3559 if $cust_bill_pkg->recur != 0;
3562 if ($type && $type eq 'R') {
3563 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3564 if $cust_bill_pkg->recur != 0;
3567 if ($type && $type eq 'U') {
3568 $subtotal{$section} += $usage
3569 unless scalar(@$extra_sections);
3578 %pkg_category_cache = ();
3580 push @$late, map { { 'description' => &{$escape}($_),
3581 'subtotal' => $late_subtotal{$_},
3583 'sort_weight' => ( _pkg_category($_)
3584 ? _pkg_category($_)->weight
3587 ((_pkg_category($_) && _pkg_category($_)->condense)
3588 ? $self->_condense_section($format)
3592 sort _sectionsort keys %late_subtotal;
3595 if ( $summarypage ) {
3596 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3597 map { $_->categoryname } qsearch('pkg_category', {});
3598 push @sections, '' if exists($subtotal{''});
3600 @sections = keys %subtotal;
3603 my @early = map { { 'description' => &{$escape}($_),
3604 'subtotal' => $subtotal{$_},
3605 'summarized' => $not_tax{$_} ? '' : 'Y',
3606 'tax_section' => $not_tax{$_} ? '' : 'Y',
3607 'sort_weight' => ( _pkg_category($_)
3608 ? _pkg_category($_)->weight
3611 ((_pkg_category($_) && _pkg_category($_)->condense)
3612 ? $self->_condense_section($format)
3617 push @early, @$extra_sections if $extra_sections;
3619 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3623 #helper subs for above
3626 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3630 my $categoryname = shift;
3631 $pkg_category_cache{$categoryname} ||=
3632 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3635 my %condensed_format = (
3636 'label' => [ qw( Description Qty Amount ) ],
3638 sub { shift->{description} },
3639 sub { shift->{quantity} },
3640 sub { my($href, %opt) = @_;
3641 ($opt{dollar} || ''). $href->{amount};
3644 'align' => [ qw( l r r ) ],
3645 'span' => [ qw( 5 1 1 ) ], # unitprices?
3646 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3649 sub _condense_section {
3650 my ( $self, $format ) = ( shift, shift );
3652 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3653 qw( description_generator
3656 total_line_generator
3661 sub _condensed_generator_defaults {
3662 my ( $self, $format ) = ( shift, shift );
3663 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3672 sub _condensed_header_generator {
3673 my ( $self, $format ) = ( shift, shift );
3675 my ( $f, $prefix, $suffix, $separator, $column ) =
3676 _condensed_generator_defaults($format);
3678 if ($format eq 'latex') {
3679 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3680 $suffix = "\\\\\n\\hline";
3683 sub { my ($d,$a,$s,$w) = @_;
3684 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3686 } elsif ( $format eq 'html' ) {
3687 $prefix = '<th></th>';
3691 sub { my ($d,$a,$s,$w) = @_;
3692 return qq!<th align="$html_align{$a}">$d</th>!;
3700 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3702 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3705 $prefix. join($separator, @result). $suffix;
3710 sub _condensed_description_generator {
3711 my ( $self, $format ) = ( shift, shift );
3713 my ( $f, $prefix, $suffix, $separator, $column ) =
3714 _condensed_generator_defaults($format);
3716 my $money_char = '$';
3717 if ($format eq 'latex') {
3718 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3720 $separator = " & \n";
3722 sub { my ($d,$a,$s,$w) = @_;
3723 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3725 $money_char = '\\dollar';
3726 }elsif ( $format eq 'html' ) {
3727 $prefix = '"><td align="center"></td>';
3731 sub { my ($d,$a,$s,$w) = @_;
3732 return qq!<td align="$html_align{$a}">$d</td>!;
3734 #$money_char = $conf->config('money_char') || '$';
3735 $money_char = ''; # this is madness
3743 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3745 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3747 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3748 map { $f->{$_}->[$i] } qw(align span width)
3752 $prefix. join( $separator, @result ). $suffix;
3757 sub _condensed_total_generator {
3758 my ( $self, $format ) = ( shift, shift );
3760 my ( $f, $prefix, $suffix, $separator, $column ) =
3761 _condensed_generator_defaults($format);
3764 if ($format eq 'latex') {
3767 $separator = " & \n";
3769 sub { my ($d,$a,$s,$w) = @_;
3770 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3772 }elsif ( $format eq 'html' ) {
3776 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3778 sub { my ($d,$a,$s,$w) = @_;
3779 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3788 # my $r = &{$f->{fields}->[$i]}(@args);
3789 # $r .= ' Total' unless $i;
3791 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3793 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3794 map { $f->{$_}->[$i] } qw(align span width)
3798 $prefix. join( $separator, @result ). $suffix;
3803 =item total_line_generator FORMAT
3805 Returns a coderef used for generation of invoice total line items for this
3806 usage_class. FORMAT is either html or latex
3810 # should not be used: will have issues with hash element names (description vs
3811 # total_item and amount vs total_amount -- another array of functions?
3813 sub _condensed_total_line_generator {
3814 my ( $self, $format ) = ( shift, shift );
3816 my ( $f, $prefix, $suffix, $separator, $column ) =
3817 _condensed_generator_defaults($format);
3820 if ($format eq 'latex') {
3823 $separator = " & \n";
3825 sub { my ($d,$a,$s,$w) = @_;
3826 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3828 }elsif ( $format eq 'html' ) {
3832 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3834 sub { my ($d,$a,$s,$w) = @_;
3835 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3844 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3846 &{$column}( &{$f->{fields}->[$i]}(@args),
3847 map { $f->{$_}->[$i] } qw(align span width)
3851 $prefix. join( $separator, @result ). $suffix;
3856 #sub _items_extra_usage_sections {
3858 # my $escape = shift;
3860 # my %sections = ();
3862 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3863 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3865 # next unless $cust_bill_pkg->pkgnum > 0;
3867 # foreach my $section ( keys %usage_class ) {
3869 # my $usage = $cust_bill_pkg->usage($section);
3871 # next unless $usage && $usage > 0;
3873 # $sections{$section} ||= 0;
3874 # $sections{$section} += $usage;
3880 # map { { 'description' => &{$escape}($_),
3881 # 'subtotal' => $sections{$_},
3882 # 'summarized' => '',
3883 # 'tax_section' => '',
3886 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3890 sub _items_extra_usage_sections {
3899 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3900 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3901 next unless $cust_bill_pkg->pkgnum > 0;
3903 foreach my $classnum ( keys %usage_class ) {
3904 my $section = $usage_class{$classnum}->classname;
3905 $classnums{$section} = $classnum;
3907 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3908 my $amount = $detail->amount;
3909 next unless $amount && $amount > 0;
3911 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3912 $sections{$section}{amount} += $amount; #subtotal
3913 $sections{$section}{calls}++;
3914 $sections{$section}{duration} += $detail->duration;
3916 my $desc = $detail->regionname;
3917 my $description = $desc;
3918 $description = substr($desc, 0, 50). '...'
3919 if $format eq 'latex' && length($desc) > 50;
3921 $lines{$section}{$desc} ||= {
3922 description => &{$escape}($description),
3923 #pkgpart => $part_pkg->pkgpart,
3924 pkgnum => $cust_bill_pkg->pkgnum,
3929 #unit_amount => $cust_bill_pkg->unitrecur,
3930 quantity => $cust_bill_pkg->quantity,
3931 product_code => 'N/A',
3932 ext_description => [],
3935 $lines{$section}{$desc}{amount} += $amount;
3936 $lines{$section}{$desc}{calls}++;
3937 $lines{$section}{$desc}{duration} += $detail->duration;
3943 my %sectionmap = ();
3944 foreach (keys %sections) {
3945 my $usage_class = $usage_class{$classnums{$_}};
3946 $sectionmap{$_} = { 'description' => &{$escape}($_),
3947 'amount' => $sections{$_}{amount}, #subtotal
3948 'calls' => $sections{$_}{calls},
3949 'duration' => $sections{$_}{duration},
3951 'tax_section' => '',
3952 'sort_weight' => $usage_class->weight,
3953 ( $usage_class->format
3954 ? ( map { $_ => $usage_class->$_($format) }
3955 qw( description_generator header_generator total_generator total_line_generator )
3962 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3966 foreach my $section ( keys %lines ) {
3967 foreach my $line ( keys %{$lines{$section}} ) {
3968 my $l = $lines{$section}{$line};
3969 $l->{section} = $sectionmap{$section};
3970 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3971 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3976 return(\@sections, \@lines);
3982 my $end = $self->_date;
3983 my $start = $end - 2592000; # 30 days
3984 my $cust_main = $self->cust_main;
3985 my @pkgs = $cust_main->all_pkgs;
3986 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3989 foreach my $pkg ( @pkgs ) {
3990 my @h_cust_svc = $pkg->h_cust_svc($end);
3991 foreach my $h_cust_svc ( @h_cust_svc ) {
3992 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3993 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3995 my $inserted = $h_cust_svc->date_inserted;
3996 my $deleted = $h_cust_svc->date_deleted;
3997 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3999 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4001 # DID either activated or ported in; cannot be both for same DID simultaneously
4002 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4003 && (!$phone_inserted->lnp_status
4004 || $phone_inserted->lnp_status eq ''
4005 || $phone_inserted->lnp_status eq 'native')) {
4008 else { # this one not so clean, should probably move to (h_)svc_phone
4009 my $phone_portedin = qsearchs( 'h_svc_phone',
4010 { 'svcnum' => $h_cust_svc->svcnum,
4011 'lnp_status' => 'portedin' },
4012 FS::h_svc_phone->sql_h_searchs($end),
4014 $num_portedin++ if $phone_portedin;
4017 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4018 if($deleted >= $start && $deleted <= $end && $phone_deleted
4019 && (!$phone_deleted->lnp_status
4020 || $phone_deleted->lnp_status ne 'portingout')) {
4023 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4024 && $phone_deleted->lnp_status
4025 && $phone_deleted->lnp_status eq 'portingout') {
4029 # increment usage minutes
4030 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4031 foreach my $cdr ( @cdrs ) {
4032 $minutes += $cdr->billsec/60;
4035 # don't look at this service again
4036 push @seen, $h_cust_svc->svcnum;
4040 $minutes = sprintf("%d", $minutes);
4041 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4042 . "$num_deactivated Ported-Out: $num_portedout ",
4043 "Total Minutes: $minutes");
4046 sub _items_svc_phone_sections {
4055 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4056 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4058 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4059 next unless $cust_bill_pkg->pkgnum > 0;
4061 my @header = $cust_bill_pkg->details_header;
4062 next unless scalar(@header);
4064 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4066 my $phonenum = $detail->phonenum;
4067 next unless $phonenum;
4069 my $amount = $detail->amount;
4070 next unless $amount && $amount > 0;
4072 $sections{$phonenum} ||= { 'amount' => 0,
4075 'sort_weight' => -1,
4076 'phonenum' => $phonenum,
4078 $sections{$phonenum}{amount} += $amount; #subtotal
4079 $sections{$phonenum}{calls}++;
4080 $sections{$phonenum}{duration} += $detail->duration;
4082 my $desc = $detail->regionname;
4083 my $description = $desc;
4084 $description = substr($desc, 0, 50). '...'
4085 if $format eq 'latex' && length($desc) > 50;
4087 $lines{$phonenum}{$desc} ||= {
4088 description => &{$escape}($description),
4089 #pkgpart => $part_pkg->pkgpart,
4097 product_code => 'N/A',
4098 ext_description => [],
4101 $lines{$phonenum}{$desc}{amount} += $amount;
4102 $lines{$phonenum}{$desc}{calls}++;
4103 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4105 my $line = $usage_class{$detail->classnum}->classname;
4106 $sections{"$phonenum $line"} ||=
4110 'sort_weight' => $usage_class{$detail->classnum}->weight,
4111 'phonenum' => $phonenum,
4112 'header' => [ @header ],
4114 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4115 $sections{"$phonenum $line"}{calls}++;
4116 $sections{"$phonenum $line"}{duration} += $detail->duration;
4118 $lines{"$phonenum $line"}{$desc} ||= {
4119 description => &{$escape}($description),
4120 #pkgpart => $part_pkg->pkgpart,
4128 product_code => 'N/A',
4129 ext_description => [],
4132 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4133 $lines{"$phonenum $line"}{$desc}{calls}++;
4134 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4135 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4136 $detail->formatted('format' => $format);
4141 my %sectionmap = ();
4142 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4143 foreach ( keys %sections ) {
4144 my @header = @{ $sections{$_}{header} || [] };
4146 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4147 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4148 my $usage_class = $summary ? $simple : $usage_simple;
4149 my $ending = $summary ? ' usage charges' : '';
4152 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4154 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4155 'amount' => $sections{$_}{amount}, #subtotal
4156 'calls' => $sections{$_}{calls},
4157 'duration' => $sections{$_}{duration},
4159 'tax_section' => '',
4160 'phonenum' => $sections{$_}{phonenum},
4161 'sort_weight' => $sections{$_}{sort_weight},
4162 'post_total' => $summary, #inspire pagebreak
4164 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4165 qw( description_generator
4168 total_line_generator
4175 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4176 $a->{sort_weight} <=> $b->{sort_weight}
4181 foreach my $section ( keys %lines ) {
4182 foreach my $line ( keys %{$lines{$section}} ) {
4183 my $l = $lines{$section}{$line};
4184 $l->{section} = $sectionmap{$section};
4185 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4186 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4191 if($conf->exists('phone_usage_class_summary')) {
4192 # this only works with Latex
4196 # after this, we'll have only two sections per DID:
4197 # Calls Summary and Calls Detail
4198 foreach my $section ( @sections ) {
4199 if($section->{'post_total'}) {
4200 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4201 $section->{'total_line_generator'} = sub { '' };
4202 $section->{'total_generator'} = sub { '' };
4203 $section->{'header_generator'} = sub { '' };
4204 $section->{'description_generator'} = '';
4205 push @newsections, $section;
4206 my %calls_detail = %$section;
4207 $calls_detail{'post_total'} = '';
4208 $calls_detail{'sort_weight'} = '';
4209 $calls_detail{'description_generator'} = sub { '' };
4210 $calls_detail{'header_generator'} = sub {
4211 return ' & Date/Time & Called Number & Duration & Price'
4212 if $format eq 'latex';
4215 $calls_detail{'description'} = 'Calls Detail: '
4216 . $section->{'phonenum'};
4217 push @newsections, \%calls_detail;
4221 # after this, each usage class is collapsed/summarized into a single
4222 # line under the Calls Summary section
4223 foreach my $newsection ( @newsections ) {
4224 if($newsection->{'post_total'}) { # this means Calls Summary
4225 foreach my $section ( @sections ) {
4226 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4227 && !$section->{'post_total'});
4228 my $newdesc = $section->{'description'};
4229 my $tn = $section->{'phonenum'};
4230 $newdesc =~ s/$tn//g;
4231 my $line = { ext_description => [],
4235 calls => $section->{'calls'},
4236 section => $newsection,
4237 duration => $section->{'duration'},
4238 description => $newdesc,
4239 amount => sprintf("%.2f",$section->{'amount'}),
4240 product_code => 'N/A',
4242 push @newlines, $line;
4247 # after this, Calls Details is populated with all CDRs
4248 foreach my $newsection ( @newsections ) {
4249 if(!$newsection->{'post_total'}) { # this means Calls Details
4250 foreach my $line ( @lines ) {
4251 next unless (scalar(@{$line->{'ext_description'}}) &&
4252 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4254 my @extdesc = @{$line->{'ext_description'}};
4256 foreach my $extdesc ( @extdesc ) {
4257 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4258 push @newextdesc, $extdesc;
4260 $line->{'ext_description'} = \@newextdesc;
4261 $line->{'section'} = $newsection;
4262 push @newlines, $line;
4267 return(\@newsections, \@newlines);
4270 return(\@sections, \@lines);
4277 #my @display = scalar(@_)
4279 # : qw( _items_previous _items_pkg );
4280 # #: qw( _items_pkg );
4281 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4282 my @display = qw( _items_previous _items_pkg );
4285 foreach my $display ( @display ) {
4286 push @b, $self->$display(@_);
4291 sub _items_previous {
4293 my $cust_main = $self->cust_main;
4294 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4296 foreach ( @pr_cust_bill ) {
4297 my $date = $conf->exists('invoice_show_prior_due_date')
4298 ? 'due '. $_->due_date2str($date_format)
4299 : time2str($date_format, $_->_date);
4301 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4302 #'pkgpart' => 'N/A',
4304 'amount' => sprintf("%.2f", $_->owed),
4310 # 'description' => 'Previous Balance',
4311 # #'pkgpart' => 'N/A',
4312 # 'pkgnum' => 'N/A',
4313 # 'amount' => sprintf("%10.2f", $pr_total ),
4314 # 'ext_description' => [ map {
4315 # "Invoice ". $_->invnum.
4316 # " (". time2str("%x",$_->_date). ") ".
4317 # sprintf("%10.2f", $_->owed)
4318 # } @pr_cust_bill ],
4327 warn "$me _items_pkg searching for all package line items\n"
4330 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4332 warn "$me _items_pkg filtering line items\n"
4334 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4336 if ($options{section} && $options{section}->{condensed}) {
4338 warn "$me _items_pkg condensing section\n"
4342 local $Storable::canonical = 1;
4343 foreach ( @items ) {
4345 delete $item->{ref};
4346 delete $item->{ext_description};
4347 my $key = freeze($item);
4348 $itemshash{$key} ||= 0;
4349 $itemshash{$key} ++; # += $item->{quantity};
4351 @items = sort { $a->{description} cmp $b->{description} }
4352 map { my $i = thaw($_);
4353 $i->{quantity} = $itemshash{$_};
4355 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4361 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4368 return 0 unless $a->itemdesc cmp $b->itemdesc;
4369 return -1 if $b->itemdesc eq 'Tax';
4370 return 1 if $a->itemdesc eq 'Tax';
4371 return -1 if $b->itemdesc eq 'Other surcharges';
4372 return 1 if $a->itemdesc eq 'Other surcharges';
4373 $a->itemdesc cmp $b->itemdesc;
4378 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4379 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4382 sub _items_cust_bill_pkg {
4384 my $cust_bill_pkgs = shift;
4387 my $format = $opt{format} || '';
4388 my $escape_function = $opt{escape_function} || sub { shift };
4389 my $format_function = $opt{format_function} || '';
4390 my $unsquelched = $opt{unsquelched} || '';
4391 my $section = $opt{section}->{description} if $opt{section};
4392 my $summary_page = $opt{summary_page} || '';
4393 my $multilocation = $opt{multilocation} || '';
4394 my $multisection = $opt{multisection} || '';
4395 my $discount_show_always = 0;
4398 my ($s, $r, $u) = ( undef, undef, undef );
4399 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4402 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4405 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4406 && $conf->exists('discount-show-always'));
4408 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4409 if ( $_ && !$cust_bill_pkg->hidden ) {
4410 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4411 $_->{amount} =~ s/^\-0\.00$/0.00/;
4412 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4414 unless ( $_->{amount} == 0 && !$discount_show_always );
4419 foreach my $display ( grep { defined($section)
4420 ? $_->section eq $section
4423 #grep { !$_->summary || !$summary_page } # bunk!
4424 grep { !$_->summary || $multisection }
4425 $cust_bill_pkg->cust_bill_pkg_display
4429 warn "$me _items_cust_bill_pkg considering display item $display\n"
4432 my $type = $display->type;
4434 my $desc = $cust_bill_pkg->desc;
4435 $desc = substr($desc, 0, 50). '...'
4436 if $format eq 'latex' && length($desc) > 50;
4438 my %details_opt = ( 'format' => $format,
4439 'escape_function' => $escape_function,
4440 'format_function' => $format_function,
4443 if ( $cust_bill_pkg->pkgnum > 0 ) {
4445 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4448 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4450 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4452 warn "$me _items_cust_bill_pkg adding setup\n"
4455 my $description = $desc;
4456 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4459 unless ( $cust_pkg->part_pkg->hide_svc_detail
4460 || $cust_bill_pkg->hidden )
4463 push @d, map &{$escape_function}($_),
4464 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4465 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4467 if ( $multilocation ) {
4468 my $loc = $cust_pkg->location_label;
4469 $loc = substr($loc, 0, 50). '...'
4470 if $format eq 'latex' && length($loc) > 50;
4471 push @d, &{$escape_function}($loc);
4476 push @d, $cust_bill_pkg->details(%details_opt)
4477 if $cust_bill_pkg->recur == 0;
4479 if ( $cust_bill_pkg->hidden ) {
4480 $s->{amount} += $cust_bill_pkg->setup;
4481 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4482 push @{ $s->{ext_description} }, @d;
4485 description => $description,
4486 #pkgpart => $part_pkg->pkgpart,
4487 pkgnum => $cust_bill_pkg->pkgnum,
4488 amount => $cust_bill_pkg->setup,
4489 unit_amount => $cust_bill_pkg->unitsetup,
4490 quantity => $cust_bill_pkg->quantity,
4491 ext_description => \@d,
4497 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4498 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4499 ( !$type || $type eq 'R' || $type eq 'U' )
4503 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4506 my $is_summary = $display->summary;
4507 my $description = ($is_summary && $type && $type eq 'U')
4508 ? "Usage charges" : $desc;
4510 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4511 " - ". time2str($date_format, $cust_bill_pkg->edate).
4513 unless $conf->exists('disable_line_item_date_ranges');
4517 #at least until cust_bill_pkg has "past" ranges in addition to
4518 #the "future" sdate/edate ones... see #3032
4519 my @dates = ( $self->_date );
4520 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4521 push @dates, $prev->sdate if $prev;
4522 push @dates, undef if !$prev;
4524 unless ( $cust_pkg->part_pkg->hide_svc_detail
4525 || $cust_bill_pkg->itemdesc
4526 || $cust_bill_pkg->hidden
4527 || $is_summary && $type && $type eq 'U' )
4530 warn "$me _items_cust_bill_pkg adding service details\n"
4533 push @d, map &{$escape_function}($_),
4534 $cust_pkg->h_labels_short(@dates, 'I')
4535 #$cust_bill_pkg->edate,
4536 #$cust_bill_pkg->sdate)
4537 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4539 warn "$me _items_cust_bill_pkg done adding service details\n"
4542 if ( $multilocation ) {
4543 my $loc = $cust_pkg->location_label;
4544 $loc = substr($loc, 0, 50). '...'
4545 if $format eq 'latex' && length($loc) > 50;
4546 push @d, &{$escape_function}($loc);
4551 warn "$me _items_cust_bill_pkg adding details\n"
4554 push @d, $cust_bill_pkg->details(%details_opt)
4555 unless ($is_summary || $type && $type eq 'R');
4557 warn "$me _items_cust_bill_pkg calculating amount\n"
4562 $amount = $cust_bill_pkg->recur;
4563 }elsif($type eq 'R') {
4564 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4565 }elsif($type eq 'U') {
4566 $amount = $cust_bill_pkg->usage;
4569 if ( !$type || $type eq 'R' ) {
4571 warn "$me _items_cust_bill_pkg adding recur\n"
4574 if ( $cust_bill_pkg->hidden ) {
4575 $r->{amount} += $amount;
4576 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4577 push @{ $r->{ext_description} }, @d;
4580 description => $description,
4581 #pkgpart => $part_pkg->pkgpart,
4582 pkgnum => $cust_bill_pkg->pkgnum,
4584 unit_amount => $cust_bill_pkg->unitrecur,
4585 quantity => $cust_bill_pkg->quantity,
4586 ext_description => \@d,
4590 } else { # $type eq 'U'
4592 warn "$me _items_cust_bill_pkg adding usage\n"
4595 if ( $cust_bill_pkg->hidden ) {
4596 $u->{amount} += $amount;
4597 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4598 push @{ $u->{ext_description} }, @d;
4601 description => $description,
4602 #pkgpart => $part_pkg->pkgpart,
4603 pkgnum => $cust_bill_pkg->pkgnum,
4605 unit_amount => $cust_bill_pkg->unitrecur,
4606 quantity => $cust_bill_pkg->quantity,
4607 ext_description => \@d,
4613 } # recurring or usage with recurring charge
4615 } else { #pkgnum tax or one-shot line item (??)
4617 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4620 if ( $cust_bill_pkg->setup != 0 ) {
4622 'description' => $desc,
4623 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4626 if ( $cust_bill_pkg->recur != 0 ) {
4628 'description' => "$desc (".
4629 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4630 time2str($date_format, $cust_bill_pkg->edate). ')',
4631 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4641 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4644 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4646 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4647 $_->{amount} =~ s/^\-0\.00$/0.00/;
4648 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4650 unless ( $_->{amount} == 0 && !$discount_show_always );
4658 sub _items_credits {
4659 my( $self, %opt ) = @_;
4660 my $trim_len = $opt{'trim_len'} || 60;
4664 foreach ( $self->cust_credited ) {
4666 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4668 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4669 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4670 $reason = " ($reason) " if $reason;
4673 #'description' => 'Credit ref\#'. $_->crednum.
4674 # " (". time2str("%x",$_->cust_credit->_date) .")".
4676 'description' => 'Credit applied '.
4677 time2str($date_format,$_->cust_credit->_date). $reason,
4678 'amount' => sprintf("%.2f",$_->amount),
4686 sub _items_payments {
4690 #get & print payments
4691 foreach ( $self->cust_bill_pay ) {
4693 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4696 'description' => "Payment received ".
4697 time2str($date_format,$_->cust_pay->_date ),
4698 'amount' => sprintf("%.2f", $_->amount )
4706 =item call_details [ OPTION => VALUE ... ]
4708 Returns an array of CSV strings representing the call details for this invoice
4709 The only option available is the boolean prepend_billed_number
4714 my ($self, %opt) = @_;
4716 my $format_function = sub { shift };
4718 if ($opt{prepend_billed_number}) {
4719 $format_function = sub {
4723 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4728 my @details = map { $_->details( 'format_function' => $format_function,
4729 'escape_function' => sub{ return() },
4733 $self->cust_bill_pkg;
4734 my $header = $details[0];
4735 ( $header, grep { $_ ne $header } @details );
4745 =item process_reprint
4749 sub process_reprint {
4750 process_re_X('print', @_);
4753 =item process_reemail
4757 sub process_reemail {
4758 process_re_X('email', @_);
4766 process_re_X('fax', @_);
4774 process_re_X('ftp', @_);
4781 sub process_respool {
4782 process_re_X('spool', @_);
4785 use Storable qw(thaw);
4789 my( $method, $job ) = ( shift, shift );
4790 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4792 my $param = thaw(decode_base64(shift));
4793 warn Dumper($param) if $DEBUG;
4804 my($method, $job, %param ) = @_;
4806 warn "re_X $method for job $job with param:\n".
4807 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4810 #some false laziness w/search/cust_bill.html
4812 my $orderby = 'ORDER BY cust_bill._date';
4814 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4816 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4818 my @cust_bill = qsearch( {
4819 #'select' => "cust_bill.*",
4820 'table' => 'cust_bill',
4821 'addl_from' => $addl_from,
4823 'extra_sql' => $extra_sql,
4824 'order_by' => $orderby,
4828 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4830 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4833 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4834 foreach my $cust_bill ( @cust_bill ) {
4835 $cust_bill->$method();
4837 if ( $job ) { #progressbar foo
4839 if ( time - $min_sec > $last ) {
4840 my $error = $job->update_statustext(
4841 int( 100 * $num / scalar(@cust_bill) )
4843 die $error if $error;
4854 =head1 CLASS METHODS
4860 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4865 my ($class, $start, $end) = @_;
4867 $class->paid_sql($start, $end). ' - '.
4868 $class->credited_sql($start, $end);
4873 Returns an SQL fragment to retreive the net amount (charged minus credited).
4878 my ($class, $start, $end) = @_;
4879 'charged - '. $class->credited_sql($start, $end);
4884 Returns an SQL fragment to retreive the amount paid against this invoice.
4889 my ($class, $start, $end) = @_;
4890 $start &&= "AND cust_bill_pay._date <= $start";
4891 $end &&= "AND cust_bill_pay._date > $end";
4892 $start = '' unless defined($start);
4893 $end = '' unless defined($end);
4894 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4895 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4900 Returns an SQL fragment to retreive the amount credited against this invoice.
4905 my ($class, $start, $end) = @_;
4906 $start &&= "AND cust_credit_bill._date <= $start";
4907 $end &&= "AND cust_credit_bill._date > $end";
4908 $start = '' unless defined($start);
4909 $end = '' unless defined($end);
4910 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4911 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4916 Returns an SQL fragment to retrieve the due date of an invoice.
4917 Currently only supported on PostgreSQL.
4925 cust_bill.invoice_terms,
4926 cust_main.invoice_terms,
4927 \''.($conf->config('invoice_default_terms') || '').'\'
4928 ), E\'Net (\\\\d+)\'
4930 ) * 86400 + cust_bill._date'
4933 =item search_sql_where HASHREF
4935 Class method which returns an SQL WHERE fragment to search for parameters
4936 specified in HASHREF. Valid parameters are
4942 List reference of start date, end date, as UNIX timestamps.
4952 List reference of charged limits (exclusive).
4956 List reference of charged limits (exclusive).
4960 flag, return open invoices only
4964 flag, return net invoices only
4968 =item newest_percust
4972 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4976 sub search_sql_where {
4977 my($class, $param) = @_;
4979 warn "$me search_sql_where called with params: \n".
4980 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4986 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4987 push @search, "cust_main.agentnum = $1";
4991 if ( $param->{_date} ) {
4992 my($beginning, $ending) = @{$param->{_date}};
4994 push @search, "cust_bill._date >= $beginning",
4995 "cust_bill._date < $ending";
4999 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5000 push @search, "cust_bill.invnum >= $1";
5002 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5003 push @search, "cust_bill.invnum <= $1";
5007 if ( $param->{charged} ) {
5008 my @charged = ref($param->{charged})
5009 ? @{ $param->{charged} }
5010 : ($param->{charged});
5012 push @search, map { s/^charged/cust_bill.charged/; $_; }
5016 my $owed_sql = FS::cust_bill->owed_sql;
5019 if ( $param->{owed} ) {
5020 my @owed = ref($param->{owed})
5021 ? @{ $param->{owed} }
5023 push @search, map { s/^owed/$owed_sql/; $_; }
5028 push @search, "0 != $owed_sql"
5029 if $param->{'open'};
5030 push @search, '0 != '. FS::cust_bill->net_sql
5034 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5035 if $param->{'days'};
5038 if ( $param->{'newest_percust'} ) {
5040 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5041 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5043 my @newest_where = map { my $x = $_;
5044 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5047 grep ! /^cust_main./, @search;
5048 my $newest_where = scalar(@newest_where)
5049 ? ' AND '. join(' AND ', @newest_where)
5053 push @search, "cust_bill._date = (
5054 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5055 WHERE newest_cust_bill.custnum = cust_bill.custnum
5061 #agent virtualization
5062 my $curuser = $FS::CurrentUser::CurrentUser;
5063 if ( $curuser->username eq 'fs_queue'
5064 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5066 my $newuser = qsearchs('access_user', {
5067 'username' => $username,
5071 $curuser = $newuser;
5073 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5076 push @search, $curuser->agentnums_sql;
5078 join(' AND ', @search );
5090 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5091 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base