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
2555 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2556 $max_edate = $cust_bill_pkg->edate
2557 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2560 $invoice_data{'bill_period'} = '';
2561 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2562 . " to " . time2str('%e %h', $max_edate)
2563 if ($max_edate != 0 && $min_sdate != 999999999999);
2565 $invoice_data{finance_section} = '';
2566 if ( $conf->config('finance_pkgclass') ) {
2568 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2569 $invoice_data{finance_section} = $pkg_class->categoryname;
2571 $invoice_data{finance_amount} = '0.00';
2572 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2574 my $countrydefault = $conf->config('countrydefault') || 'US';
2575 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2576 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2577 my $method = $prefix.$_;
2578 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2580 $invoice_data{'ship_country'} = ''
2581 if ( $invoice_data{'ship_country'} eq $countrydefault );
2583 $invoice_data{'cid'} = $params{'cid'}
2586 if ( $cust_main->country eq $countrydefault ) {
2587 $invoice_data{'country'} = '';
2589 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2593 $invoice_data{'address'} = \@address;
2595 $cust_main->payname.
2596 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2597 ? " (P.O. #". $cust_main->payinfo. ")"
2601 push @address, $cust_main->company
2602 if $cust_main->company;
2603 push @address, $cust_main->address1;
2604 push @address, $cust_main->address2
2605 if $cust_main->address2;
2607 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2608 push @address, $invoice_data{'country'}
2609 if $invoice_data{'country'};
2611 while (scalar(@address) < 5);
2613 $invoice_data{'logo_file'} = $params{'logo_file'}
2614 if $params{'logo_file'};
2615 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2616 if $params{'barcode_file'};
2617 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2618 if $params{'barcode_img'};
2619 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2620 if $params{'barcode_cid'};
2622 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2623 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2624 #my $balance_due = $self->owed + $pr_total - $cr_total;
2625 my $balance_due = $self->owed + $pr_total;
2626 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2627 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2628 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2629 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2631 my $summarypage = '';
2632 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2635 $invoice_data{'summarypage'} = $summarypage;
2637 warn "$me substituting variables in notes, footer, smallfooter\n"
2640 foreach my $include (qw( notes footer smallfooter coupon )) {
2642 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2645 if ( $conf->exists($inc_file, $agentnum)
2646 && length( $conf->config($inc_file, $agentnum) ) ) {
2648 @inc_src = $conf->config($inc_file, $agentnum);
2652 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2654 my $convert_map = $convert_maps{$format}{$include};
2656 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2657 s/--\@\]/$delimiters{$format}[1]/g;
2660 &$convert_map( $conf->config($inc_file, $agentnum) );
2664 my $inc_tt = new Text::Template (
2666 SOURCE => [ map "$_\n", @inc_src ],
2667 DELIMITERS => $delimiters{$format},
2668 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2670 unless ( $inc_tt->compile() ) {
2671 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2672 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2676 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2678 $invoice_data{$include} =~ s/\n+$//
2679 if ($format eq 'latex');
2682 $invoice_data{'po_line'} =
2683 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2684 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2687 my %money_chars = ( 'latex' => '',
2688 'html' => $conf->config('money_char') || '$',
2691 my $money_char = $money_chars{$format};
2693 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2694 'html' => $conf->config('money_char') || '$',
2697 my $other_money_char = $other_money_chars{$format};
2698 $invoice_data{'dollar'} = $other_money_char;
2700 my @detail_items = ();
2701 my @total_items = ();
2705 $invoice_data{'detail_items'} = \@detail_items;
2706 $invoice_data{'total_items'} = \@total_items;
2707 $invoice_data{'buf'} = \@buf;
2708 $invoice_data{'sections'} = \@sections;
2710 warn "$me generating sections\n"
2713 my $previous_section = { 'description' => 'Previous Charges',
2714 'subtotal' => $other_money_char.
2715 sprintf('%.2f', $pr_total),
2716 'summarized' => $summarypage ? 'Y' : '',
2718 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2719 join(' / ', map { $cust_main->balance_date_range(@$_) }
2720 $self->_prior_month30s
2722 if $conf->exists('invoice_include_aging');
2725 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2726 'subtotal' => $taxtotal, # adjusted below
2727 'summarized' => $summarypage ? 'Y' : '',
2729 my $tax_weight = _pkg_category($tax_section->{description})
2730 ? _pkg_category($tax_section->{description})->weight
2732 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2733 $tax_section->{'sort_weight'} = $tax_weight;
2736 my $adjusttotal = 0;
2737 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2738 'subtotal' => 0, # adjusted below
2739 'summarized' => $summarypage ? 'Y' : '',
2741 my $adjust_weight = _pkg_category($adjust_section->{description})
2742 ? _pkg_category($adjust_section->{description})->weight
2744 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2745 $adjust_section->{'sort_weight'} = $adjust_weight;
2747 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2748 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2749 $invoice_data{'multisection'} = $multisection;
2750 my $late_sections = [];
2751 my $extra_sections = [];
2752 my $extra_lines = ();
2753 if ( $multisection ) {
2754 ($extra_sections, $extra_lines) =
2755 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2756 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2758 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2760 push @detail_items, @$extra_lines if $extra_lines;
2762 $self->_items_sections( $late_sections, # this could stand a refactor
2764 $escape_function_nonbsp,
2768 if ($conf->exists('svc_phone_sections')) {
2769 my ($phone_sections, $phone_lines) =
2770 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2771 push @{$late_sections}, @$phone_sections;
2772 push @detail_items, @$phone_lines;
2775 push @sections, { 'description' => '', 'subtotal' => '' };
2778 unless ( $conf->exists('disable_previous_balance')
2779 || $conf->exists('previous_balance-summary_only')
2783 warn "$me adding previous balances\n"
2786 foreach my $line_item ( $self->_items_previous ) {
2789 ext_description => [],
2791 $detail->{'ref'} = $line_item->{'pkgnum'};
2792 $detail->{'quantity'} = 1;
2793 $detail->{'section'} = $previous_section;
2794 $detail->{'description'} = &$escape_function($line_item->{'description'});
2795 if ( exists $line_item->{'ext_description'} ) {
2796 @{$detail->{'ext_description'}} = map {
2797 &$escape_function($_);
2798 } @{$line_item->{'ext_description'}};
2800 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2801 $line_item->{'amount'};
2802 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2804 push @detail_items, $detail;
2805 push @buf, [ $detail->{'description'},
2806 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2812 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2813 push @buf, ['','-----------'];
2814 push @buf, [ 'Total Previous Balance',
2815 $money_char. sprintf("%10.2f", $pr_total) ];
2819 if ( $conf->exists('svc_phone-did-summary') ) {
2820 warn "$me adding DID summary\n"
2823 my ($didsummary,$minutes) = $self->_did_summary;
2824 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2826 { 'description' => $didsummary_desc,
2827 'ext_description' => [ $didsummary, $minutes ],
2832 foreach my $section (@sections, @$late_sections) {
2834 warn "$me adding section \n". Dumper($section)
2837 # begin some normalization
2838 $section->{'subtotal'} = $section->{'amount'}
2840 && !exists($section->{subtotal})
2841 && exists($section->{amount});
2843 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2844 if ( $invoice_data{finance_section} &&
2845 $section->{'description'} eq $invoice_data{finance_section} );
2847 $section->{'subtotal'} = $other_money_char.
2848 sprintf('%.2f', $section->{'subtotal'})
2851 # continue some normalization
2852 $section->{'amount'} = $section->{'subtotal'}
2856 if ( $section->{'description'} ) {
2857 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2862 warn "$me setting options\n"
2865 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2867 $options{'section'} = $section if $multisection;
2868 $options{'format'} = $format;
2869 $options{'escape_function'} = $escape_function;
2870 $options{'format_function'} = sub { () } unless $unsquelched;
2871 $options{'unsquelched'} = $unsquelched;
2872 $options{'summary_page'} = $summarypage;
2873 $options{'skip_usage'} =
2874 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2875 $options{'multilocation'} = $multilocation;
2876 $options{'multisection'} = $multisection;
2878 warn "$me searching for line items\n"
2881 foreach my $line_item ( $self->_items_pkg(%options) ) {
2883 warn "$me adding line item $line_item\n"
2887 ext_description => [],
2889 $detail->{'ref'} = $line_item->{'pkgnum'};
2890 $detail->{'quantity'} = $line_item->{'quantity'};
2891 $detail->{'section'} = $section;
2892 $detail->{'description'} = &$escape_function($line_item->{'description'});
2893 if ( exists $line_item->{'ext_description'} ) {
2894 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2896 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2897 $line_item->{'amount'};
2898 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2899 $line_item->{'unit_amount'};
2900 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2902 push @detail_items, $detail;
2903 push @buf, ( [ $detail->{'description'},
2904 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2906 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2910 if ( $section->{'description'} ) {
2911 push @buf, ( ['','-----------'],
2912 [ $section->{'description'}. ' sub-total',
2913 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2922 $invoice_data{current_less_finance} =
2923 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2925 if ( $multisection && !$conf->exists('disable_previous_balance')
2926 || $conf->exists('previous_balance-summary_only') )
2928 unshift @sections, $previous_section if $pr_total;
2931 warn "$me adding taxes\n"
2934 foreach my $tax ( $self->_items_tax ) {
2936 $taxtotal += $tax->{'amount'};
2938 my $description = &$escape_function( $tax->{'description'} );
2939 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2941 if ( $multisection ) {
2943 my $money = $old_latex ? '' : $money_char;
2944 push @detail_items, {
2945 ext_description => [],
2948 description => $description,
2949 amount => $money. $amount,
2951 section => $tax_section,
2956 push @total_items, {
2957 'total_item' => $description,
2958 'total_amount' => $other_money_char. $amount,
2963 push @buf,[ $description,
2964 $money_char. $amount,
2971 $total->{'total_item'} = 'Sub-total';
2972 $total->{'total_amount'} =
2973 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2975 if ( $multisection ) {
2976 $tax_section->{'subtotal'} = $other_money_char.
2977 sprintf('%.2f', $taxtotal);
2978 $tax_section->{'pretotal'} = 'New charges sub-total '.
2979 $total->{'total_amount'};
2980 push @sections, $tax_section if $taxtotal;
2982 unshift @total_items, $total;
2985 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2987 push @buf,['','-----------'];
2988 push @buf,[( $conf->exists('disable_previous_balance')
2990 : 'Total New Charges'
2992 $money_char. sprintf("%10.2f",$self->charged) ];
2998 $item = $conf->config('previous_balance-exclude_from_total')
2999 || 'Total New Charges'
3000 if $conf->exists('previous_balance-exclude_from_total');
3001 my $amount = $self->charged +
3002 ( $conf->exists('disable_previous_balance') ||
3003 $conf->exists('previous_balance-exclude_from_total')
3007 $total->{'total_item'} = &$embolden_function($item);
3008 $total->{'total_amount'} =
3009 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3010 if ( $multisection ) {
3011 if ( $adjust_section->{'sort_weight'} ) {
3012 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3013 sprintf("%.2f", ($self->billing_balance || 0) );
3015 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3016 sprintf('%.2f', $self->charged );
3019 push @total_items, $total;
3021 push @buf,['','-----------'];
3024 sprintf( '%10.2f', $amount )
3029 unless ( $conf->exists('disable_previous_balance') ) {
3030 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3033 my $credittotal = 0;
3034 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3037 $total->{'total_item'} = &$escape_function($credit->{'description'});
3038 $credittotal += $credit->{'amount'};
3039 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3040 $adjusttotal += $credit->{'amount'};
3041 if ( $multisection ) {
3042 my $money = $old_latex ? '' : $money_char;
3043 push @detail_items, {
3044 ext_description => [],
3047 description => &$escape_function($credit->{'description'}),
3048 amount => $money. $credit->{'amount'},
3050 section => $adjust_section,
3053 push @total_items, $total;
3057 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3060 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3061 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3065 my $paymenttotal = 0;
3066 foreach my $payment ( $self->_items_payments ) {
3068 $total->{'total_item'} = &$escape_function($payment->{'description'});
3069 $paymenttotal += $payment->{'amount'};
3070 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3071 $adjusttotal += $payment->{'amount'};
3072 if ( $multisection ) {
3073 my $money = $old_latex ? '' : $money_char;
3074 push @detail_items, {
3075 ext_description => [],
3078 description => &$escape_function($payment->{'description'}),
3079 amount => $money. $payment->{'amount'},
3081 section => $adjust_section,
3084 push @total_items, $total;
3086 push @buf, [ $payment->{'description'},
3087 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3090 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3092 if ( $multisection ) {
3093 $adjust_section->{'subtotal'} = $other_money_char.
3094 sprintf('%.2f', $adjusttotal);
3095 push @sections, $adjust_section
3096 unless $adjust_section->{sort_weight};
3101 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3102 $total->{'total_amount'} =
3103 &$embolden_function(
3104 $other_money_char. sprintf('%.2f', $summarypage
3106 $self->billing_balance
3107 : $self->owed + $pr_total
3110 if ( $multisection && !$adjust_section->{sort_weight} ) {
3111 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3112 $total->{'total_amount'};
3114 push @total_items, $total;
3116 push @buf,['','-----------'];
3117 push @buf,[$self->balance_due_msg, $money_char.
3118 sprintf("%10.2f", $balance_due ) ];
3122 if ( $multisection ) {
3123 if ($conf->exists('svc_phone_sections')) {
3125 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3126 $total->{'total_amount'} =
3127 &$embolden_function(
3128 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3130 my $last_section = pop @sections;
3131 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3132 $total->{'total_amount'};
3133 push @sections, $last_section;
3135 push @sections, @$late_sections
3139 my @includelist = ();
3140 push @includelist, 'summary' if $summarypage;
3141 foreach my $include ( @includelist ) {
3143 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3146 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3148 @inc_src = $conf->config($inc_file, $agentnum);
3152 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3154 my $convert_map = $convert_maps{$format}{$include};
3156 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3157 s/--\@\]/$delimiters{$format}[1]/g;
3160 &$convert_map( $conf->config($inc_file, $agentnum) );
3164 my $inc_tt = new Text::Template (
3166 SOURCE => [ map "$_\n", @inc_src ],
3167 DELIMITERS => $delimiters{$format},
3168 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3170 unless ( $inc_tt->compile() ) {
3171 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3172 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3176 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3178 $invoice_data{$include} =~ s/\n+$//
3179 if ($format eq 'latex');
3184 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3185 /invoice_lines\((\d*)\)/;
3186 $invoice_lines += $1 || scalar(@buf);
3189 die "no invoice_lines() functions in template?"
3190 if ( $format eq 'template' && !$wasfunc );
3192 if ($format eq 'template') {
3194 if ( $invoice_lines ) {
3195 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3196 $invoice_data{'total_pages'}++
3197 if scalar(@buf) % $invoice_lines;
3200 #setup subroutine for the template
3201 sub FS::cust_bill::_template::invoice_lines {
3202 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3204 scalar(@FS::cust_bill::_template::buf)
3205 ? shift @FS::cust_bill::_template::buf
3214 push @collect, split("\n",
3215 $text_template->fill_in( HASH => \%invoice_data,
3216 PACKAGE => 'FS::cust_bill::_template'
3219 $FS::cust_bill::_template::page++;
3221 map "$_\n", @collect;
3223 warn "filling in template for invoice ". $self->invnum. "\n"
3225 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3228 $text_template->fill_in(HASH => \%invoice_data);
3232 # helper routine for generating date ranges
3233 sub _prior_month30s {
3236 [ 1, 2592000 ], # 0-30 days ago
3237 [ 2592000, 5184000 ], # 30-60 days ago
3238 [ 5184000, 7776000 ], # 60-90 days ago
3239 [ 7776000, 0 ], # 90+ days ago
3242 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3243 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3248 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3250 Returns an postscript invoice, as a scalar.
3252 Options can be passed as a hashref (recommended) or as a list of time, template
3253 and then any key/value pairs for any other options.
3255 I<time> an optional value used to control the printing of overdue messages. The
3256 default is now. It isn't the date of the invoice; that's the `_date' field.
3257 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3258 L<Time::Local> and L<Date::Parse> for conversion functions.
3260 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3267 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3268 my $ps = generate_ps($file);
3270 unlink($barcodefile);
3275 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3277 Returns an PDF invoice, as a scalar.
3279 Options can be passed as a hashref (recommended) or as a list of time, template
3280 and then any key/value pairs for any other options.
3282 I<time> an optional value used to control the printing of overdue messages. The
3283 default is now. It isn't the date of the invoice; that's the `_date' field.
3284 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3285 L<Time::Local> and L<Date::Parse> for conversion functions.
3287 I<template>, if specified, is the name of a suffix for alternate invoices.
3289 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3296 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3297 my $pdf = generate_pdf($file);
3299 unlink($barcodefile);
3304 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3306 Returns an HTML invoice, as a scalar.
3308 I<time> an optional value used to control the printing of overdue messages. The
3309 default is now. It isn't the date of the invoice; that's the `_date' field.
3310 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3311 L<Time::Local> and L<Date::Parse> for conversion functions.
3313 I<template>, if specified, is the name of a suffix for alternate invoices.
3315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3317 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3318 when emailing the invoice as part of a multipart/related MIME email.
3326 %params = %{ shift() };
3328 $params{'time'} = shift;
3329 $params{'template'} = shift;
3330 $params{'cid'} = shift;
3333 $params{'format'} = 'html';
3335 $self->print_generic( %params );
3338 # quick subroutine for print_latex
3340 # There are ten characters that LaTeX treats as special characters, which
3341 # means that they do not simply typeset themselves:
3342 # # $ % & ~ _ ^ \ { }
3344 # TeX ignores blanks following an escaped character; if you want a blank (as
3345 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3349 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3350 $value =~ s/([<>])/\$$1\$/g;
3356 encode_entities($value);
3360 sub _html_escape_nbsp {
3361 my $value = _html_escape(shift);
3362 $value =~ s/ +/ /g;
3366 #utility methods for print_*
3368 sub _translate_old_latex_format {
3369 warn "_translate_old_latex_format called\n"
3376 if ( $line =~ /^%%Detail\s*$/ ) {
3378 push @template, q![@--!,
3379 q! foreach my $_tr_line (@detail_items) {!,
3380 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3381 q! $_tr_line->{'description'} .= !,
3382 q! "\\tabularnewline\n~~".!,
3383 q! join( "\\tabularnewline\n~~",!,
3384 q! @{$_tr_line->{'ext_description'}}!,
3388 while ( ( my $line_item_line = shift )
3389 !~ /^%%EndDetail\s*$/ ) {
3390 $line_item_line =~ s/'/\\'/g; # nice LTS
3391 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3392 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3393 push @template, " \$OUT .= '$line_item_line';";
3396 push @template, '}',
3399 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3401 push @template, '[@--',
3402 ' foreach my $_tr_line (@total_items) {';
3404 while ( ( my $total_item_line = shift )
3405 !~ /^%%EndTotalDetails\s*$/ ) {
3406 $total_item_line =~ s/'/\\'/g; # nice LTS
3407 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3408 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3409 push @template, " \$OUT .= '$total_item_line';";
3412 push @template, '}',
3416 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3417 push @template, $line;
3423 warn "$_\n" foreach @template;
3432 #check for an invoice-specific override
3433 return $self->invoice_terms if $self->invoice_terms;
3435 #check for a customer- specific override
3436 my $cust_main = $self->cust_main;
3437 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3439 #use configured default
3440 $conf->config('invoice_default_terms') || '';
3446 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3447 $duedate = $self->_date() + ( $1 * 86400 );
3454 $self->due_date ? time2str(shift, $self->due_date) : '';
3457 sub balance_due_msg {
3459 my $msg = 'Balance Due';
3460 return $msg unless $self->terms;
3461 if ( $self->due_date ) {
3462 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3463 } elsif ( $self->terms ) {
3464 $msg .= ' - '. $self->terms;
3469 sub balance_due_date {
3472 if ( $conf->exists('invoice_default_terms')
3473 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3474 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3479 =item invnum_date_pretty
3481 Returns a string with the invoice number and date, for example:
3482 "Invoice #54 (3/20/2008)"
3486 sub invnum_date_pretty {
3488 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3493 Returns a string with the date, for example: "3/20/2008"
3499 time2str($date_format, $self->_date);
3502 use vars qw(%pkg_category_cache);
3503 sub _items_sections {
3506 my $summarypage = shift;
3508 my $extra_sections = shift;
3512 my %late_subtotal = ();
3515 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3518 my $usage = $cust_bill_pkg->usage;
3520 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3521 next if ( $display->summary && $summarypage );
3523 my $section = $display->section;
3524 my $type = $display->type;
3526 $not_tax{$section} = 1
3527 unless $cust_bill_pkg->pkgnum == 0;
3529 if ( $display->post_total && !$summarypage ) {
3530 if (! $type || $type eq 'S') {
3531 $late_subtotal{$section} += $cust_bill_pkg->setup
3532 if $cust_bill_pkg->setup != 0;
3536 $late_subtotal{$section} += $cust_bill_pkg->recur
3537 if $cust_bill_pkg->recur != 0;
3540 if ($type && $type eq 'R') {
3541 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3542 if $cust_bill_pkg->recur != 0;
3545 if ($type && $type eq 'U') {
3546 $late_subtotal{$section} += $usage
3547 unless scalar(@$extra_sections);
3552 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3554 if (! $type || $type eq 'S') {
3555 $subtotal{$section} += $cust_bill_pkg->setup
3556 if $cust_bill_pkg->setup != 0;
3560 $subtotal{$section} += $cust_bill_pkg->recur
3561 if $cust_bill_pkg->recur != 0;
3564 if ($type && $type eq 'R') {
3565 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3566 if $cust_bill_pkg->recur != 0;
3569 if ($type && $type eq 'U') {
3570 $subtotal{$section} += $usage
3571 unless scalar(@$extra_sections);
3580 %pkg_category_cache = ();
3582 push @$late, map { { 'description' => &{$escape}($_),
3583 'subtotal' => $late_subtotal{$_},
3585 'sort_weight' => ( _pkg_category($_)
3586 ? _pkg_category($_)->weight
3589 ((_pkg_category($_) && _pkg_category($_)->condense)
3590 ? $self->_condense_section($format)
3594 sort _sectionsort keys %late_subtotal;
3597 if ( $summarypage ) {
3598 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3599 map { $_->categoryname } qsearch('pkg_category', {});
3600 push @sections, '' if exists($subtotal{''});
3602 @sections = keys %subtotal;
3605 my @early = map { { 'description' => &{$escape}($_),
3606 'subtotal' => $subtotal{$_},
3607 'summarized' => $not_tax{$_} ? '' : 'Y',
3608 'tax_section' => $not_tax{$_} ? '' : 'Y',
3609 'sort_weight' => ( _pkg_category($_)
3610 ? _pkg_category($_)->weight
3613 ((_pkg_category($_) && _pkg_category($_)->condense)
3614 ? $self->_condense_section($format)
3619 push @early, @$extra_sections if $extra_sections;
3621 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3625 #helper subs for above
3628 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3632 my $categoryname = shift;
3633 $pkg_category_cache{$categoryname} ||=
3634 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3637 my %condensed_format = (
3638 'label' => [ qw( Description Qty Amount ) ],
3640 sub { shift->{description} },
3641 sub { shift->{quantity} },
3642 sub { my($href, %opt) = @_;
3643 ($opt{dollar} || ''). $href->{amount};
3646 'align' => [ qw( l r r ) ],
3647 'span' => [ qw( 5 1 1 ) ], # unitprices?
3648 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3651 sub _condense_section {
3652 my ( $self, $format ) = ( shift, shift );
3654 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3655 qw( description_generator
3658 total_line_generator
3663 sub _condensed_generator_defaults {
3664 my ( $self, $format ) = ( shift, shift );
3665 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3674 sub _condensed_header_generator {
3675 my ( $self, $format ) = ( shift, shift );
3677 my ( $f, $prefix, $suffix, $separator, $column ) =
3678 _condensed_generator_defaults($format);
3680 if ($format eq 'latex') {
3681 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3682 $suffix = "\\\\\n\\hline";
3685 sub { my ($d,$a,$s,$w) = @_;
3686 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3688 } elsif ( $format eq 'html' ) {
3689 $prefix = '<th></th>';
3693 sub { my ($d,$a,$s,$w) = @_;
3694 return qq!<th align="$html_align{$a}">$d</th>!;
3702 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3704 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3707 $prefix. join($separator, @result). $suffix;
3712 sub _condensed_description_generator {
3713 my ( $self, $format ) = ( shift, shift );
3715 my ( $f, $prefix, $suffix, $separator, $column ) =
3716 _condensed_generator_defaults($format);
3718 my $money_char = '$';
3719 if ($format eq 'latex') {
3720 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3722 $separator = " & \n";
3724 sub { my ($d,$a,$s,$w) = @_;
3725 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3727 $money_char = '\\dollar';
3728 }elsif ( $format eq 'html' ) {
3729 $prefix = '"><td align="center"></td>';
3733 sub { my ($d,$a,$s,$w) = @_;
3734 return qq!<td align="$html_align{$a}">$d</td>!;
3736 #$money_char = $conf->config('money_char') || '$';
3737 $money_char = ''; # this is madness
3745 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3747 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3749 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3750 map { $f->{$_}->[$i] } qw(align span width)
3754 $prefix. join( $separator, @result ). $suffix;
3759 sub _condensed_total_generator {
3760 my ( $self, $format ) = ( shift, shift );
3762 my ( $f, $prefix, $suffix, $separator, $column ) =
3763 _condensed_generator_defaults($format);
3766 if ($format eq 'latex') {
3769 $separator = " & \n";
3771 sub { my ($d,$a,$s,$w) = @_;
3772 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3774 }elsif ( $format eq 'html' ) {
3778 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3780 sub { my ($d,$a,$s,$w) = @_;
3781 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3790 # my $r = &{$f->{fields}->[$i]}(@args);
3791 # $r .= ' Total' unless $i;
3793 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3795 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3796 map { $f->{$_}->[$i] } qw(align span width)
3800 $prefix. join( $separator, @result ). $suffix;
3805 =item total_line_generator FORMAT
3807 Returns a coderef used for generation of invoice total line items for this
3808 usage_class. FORMAT is either html or latex
3812 # should not be used: will have issues with hash element names (description vs
3813 # total_item and amount vs total_amount -- another array of functions?
3815 sub _condensed_total_line_generator {
3816 my ( $self, $format ) = ( shift, shift );
3818 my ( $f, $prefix, $suffix, $separator, $column ) =
3819 _condensed_generator_defaults($format);
3822 if ($format eq 'latex') {
3825 $separator = " & \n";
3827 sub { my ($d,$a,$s,$w) = @_;
3828 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3830 }elsif ( $format eq 'html' ) {
3834 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3836 sub { my ($d,$a,$s,$w) = @_;
3837 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3846 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3848 &{$column}( &{$f->{fields}->[$i]}(@args),
3849 map { $f->{$_}->[$i] } qw(align span width)
3853 $prefix. join( $separator, @result ). $suffix;
3858 #sub _items_extra_usage_sections {
3860 # my $escape = shift;
3862 # my %sections = ();
3864 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3865 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3867 # next unless $cust_bill_pkg->pkgnum > 0;
3869 # foreach my $section ( keys %usage_class ) {
3871 # my $usage = $cust_bill_pkg->usage($section);
3873 # next unless $usage && $usage > 0;
3875 # $sections{$section} ||= 0;
3876 # $sections{$section} += $usage;
3882 # map { { 'description' => &{$escape}($_),
3883 # 'subtotal' => $sections{$_},
3884 # 'summarized' => '',
3885 # 'tax_section' => '',
3888 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3892 sub _items_extra_usage_sections {
3901 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3902 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3903 next unless $cust_bill_pkg->pkgnum > 0;
3905 foreach my $classnum ( keys %usage_class ) {
3906 my $section = $usage_class{$classnum}->classname;
3907 $classnums{$section} = $classnum;
3909 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3910 my $amount = $detail->amount;
3911 next unless $amount && $amount > 0;
3913 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3914 $sections{$section}{amount} += $amount; #subtotal
3915 $sections{$section}{calls}++;
3916 $sections{$section}{duration} += $detail->duration;
3918 my $desc = $detail->regionname;
3919 my $description = $desc;
3920 $description = substr($desc, 0, 50). '...'
3921 if $format eq 'latex' && length($desc) > 50;
3923 $lines{$section}{$desc} ||= {
3924 description => &{$escape}($description),
3925 #pkgpart => $part_pkg->pkgpart,
3926 pkgnum => $cust_bill_pkg->pkgnum,
3931 #unit_amount => $cust_bill_pkg->unitrecur,
3932 quantity => $cust_bill_pkg->quantity,
3933 product_code => 'N/A',
3934 ext_description => [],
3937 $lines{$section}{$desc}{amount} += $amount;
3938 $lines{$section}{$desc}{calls}++;
3939 $lines{$section}{$desc}{duration} += $detail->duration;
3945 my %sectionmap = ();
3946 foreach (keys %sections) {
3947 my $usage_class = $usage_class{$classnums{$_}};
3948 $sectionmap{$_} = { 'description' => &{$escape}($_),
3949 'amount' => $sections{$_}{amount}, #subtotal
3950 'calls' => $sections{$_}{calls},
3951 'duration' => $sections{$_}{duration},
3953 'tax_section' => '',
3954 'sort_weight' => $usage_class->weight,
3955 ( $usage_class->format
3956 ? ( map { $_ => $usage_class->$_($format) }
3957 qw( description_generator header_generator total_generator total_line_generator )
3964 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3968 foreach my $section ( keys %lines ) {
3969 foreach my $line ( keys %{$lines{$section}} ) {
3970 my $l = $lines{$section}{$line};
3971 $l->{section} = $sectionmap{$section};
3972 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3973 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3978 return(\@sections, \@lines);
3984 my $end = $self->_date;
3985 my $start = $end - 2592000; # 30 days
3986 my $cust_main = $self->cust_main;
3987 my @pkgs = $cust_main->all_pkgs;
3988 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3991 foreach my $pkg ( @pkgs ) {
3992 my @h_cust_svc = $pkg->h_cust_svc($end);
3993 foreach my $h_cust_svc ( @h_cust_svc ) {
3994 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3995 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3997 my $inserted = $h_cust_svc->date_inserted;
3998 my $deleted = $h_cust_svc->date_deleted;
3999 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4001 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4003 # DID either activated or ported in; cannot be both for same DID simultaneously
4004 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4005 && (!$phone_inserted->lnp_status
4006 || $phone_inserted->lnp_status eq ''
4007 || $phone_inserted->lnp_status eq 'native')) {
4010 else { # this one not so clean, should probably move to (h_)svc_phone
4011 my $phone_portedin = qsearchs( 'h_svc_phone',
4012 { 'svcnum' => $h_cust_svc->svcnum,
4013 'lnp_status' => 'portedin' },
4014 FS::h_svc_phone->sql_h_searchs($end),
4016 $num_portedin++ if $phone_portedin;
4019 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4020 if($deleted >= $start && $deleted <= $end && $phone_deleted
4021 && (!$phone_deleted->lnp_status
4022 || $phone_deleted->lnp_status ne 'portingout')) {
4025 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4026 && $phone_deleted->lnp_status
4027 && $phone_deleted->lnp_status eq 'portingout') {
4031 # increment usage minutes
4032 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4033 foreach my $cdr ( @cdrs ) {
4034 $minutes += $cdr->billsec/60;
4037 # don't look at this service again
4038 push @seen, $h_cust_svc->svcnum;
4042 $minutes = sprintf("%d", $minutes);
4043 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4044 . "$num_deactivated Ported-Out: $num_portedout ",
4045 "Total Minutes: $minutes");
4048 sub _items_svc_phone_sections {
4057 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4058 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4060 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4061 next unless $cust_bill_pkg->pkgnum > 0;
4063 my @header = $cust_bill_pkg->details_header;
4064 next unless scalar(@header);
4066 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4068 my $phonenum = $detail->phonenum;
4069 next unless $phonenum;
4071 my $amount = $detail->amount;
4072 next unless $amount && $amount > 0;
4074 $sections{$phonenum} ||= { 'amount' => 0,
4077 'sort_weight' => -1,
4078 'phonenum' => $phonenum,
4080 $sections{$phonenum}{amount} += $amount; #subtotal
4081 $sections{$phonenum}{calls}++;
4082 $sections{$phonenum}{duration} += $detail->duration;
4084 my $desc = $detail->regionname;
4085 my $description = $desc;
4086 $description = substr($desc, 0, 50). '...'
4087 if $format eq 'latex' && length($desc) > 50;
4089 $lines{$phonenum}{$desc} ||= {
4090 description => &{$escape}($description),
4091 #pkgpart => $part_pkg->pkgpart,
4099 product_code => 'N/A',
4100 ext_description => [],
4103 $lines{$phonenum}{$desc}{amount} += $amount;
4104 $lines{$phonenum}{$desc}{calls}++;
4105 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4107 my $line = $usage_class{$detail->classnum}->classname;
4108 $sections{"$phonenum $line"} ||=
4112 'sort_weight' => $usage_class{$detail->classnum}->weight,
4113 'phonenum' => $phonenum,
4114 'header' => [ @header ],
4116 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4117 $sections{"$phonenum $line"}{calls}++;
4118 $sections{"$phonenum $line"}{duration} += $detail->duration;
4120 $lines{"$phonenum $line"}{$desc} ||= {
4121 description => &{$escape}($description),
4122 #pkgpart => $part_pkg->pkgpart,
4130 product_code => 'N/A',
4131 ext_description => [],
4134 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4135 $lines{"$phonenum $line"}{$desc}{calls}++;
4136 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4137 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4138 $detail->formatted('format' => $format);
4143 my %sectionmap = ();
4144 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4145 foreach ( keys %sections ) {
4146 my @header = @{ $sections{$_}{header} || [] };
4148 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4149 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4150 my $usage_class = $summary ? $simple : $usage_simple;
4151 my $ending = $summary ? ' usage charges' : '';
4154 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4156 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4157 'amount' => $sections{$_}{amount}, #subtotal
4158 'calls' => $sections{$_}{calls},
4159 'duration' => $sections{$_}{duration},
4161 'tax_section' => '',
4162 'phonenum' => $sections{$_}{phonenum},
4163 'sort_weight' => $sections{$_}{sort_weight},
4164 'post_total' => $summary, #inspire pagebreak
4166 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4167 qw( description_generator
4170 total_line_generator
4177 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4178 $a->{sort_weight} <=> $b->{sort_weight}
4183 foreach my $section ( keys %lines ) {
4184 foreach my $line ( keys %{$lines{$section}} ) {
4185 my $l = $lines{$section}{$line};
4186 $l->{section} = $sectionmap{$section};
4187 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4188 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4193 if($conf->exists('phone_usage_class_summary')) {
4194 # this only works with Latex
4198 # after this, we'll have only two sections per DID:
4199 # Calls Summary and Calls Detail
4200 foreach my $section ( @sections ) {
4201 if($section->{'post_total'}) {
4202 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4203 $section->{'total_line_generator'} = sub { '' };
4204 $section->{'total_generator'} = sub { '' };
4205 $section->{'header_generator'} = sub { '' };
4206 $section->{'description_generator'} = '';
4207 push @newsections, $section;
4208 my %calls_detail = %$section;
4209 $calls_detail{'post_total'} = '';
4210 $calls_detail{'sort_weight'} = '';
4211 $calls_detail{'description_generator'} = sub { '' };
4212 $calls_detail{'header_generator'} = sub {
4213 return ' & Date/Time & Called Number & Duration & Price'
4214 if $format eq 'latex';
4217 $calls_detail{'description'} = 'Calls Detail: '
4218 . $section->{'phonenum'};
4219 push @newsections, \%calls_detail;
4223 # after this, each usage class is collapsed/summarized into a single
4224 # line under the Calls Summary section
4225 foreach my $newsection ( @newsections ) {
4226 if($newsection->{'post_total'}) { # this means Calls Summary
4227 foreach my $section ( @sections ) {
4228 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4229 && !$section->{'post_total'});
4230 my $newdesc = $section->{'description'};
4231 my $tn = $section->{'phonenum'};
4232 $newdesc =~ s/$tn//g;
4233 my $line = { ext_description => [],
4237 calls => $section->{'calls'},
4238 section => $newsection,
4239 duration => $section->{'duration'},
4240 description => $newdesc,
4241 amount => sprintf("%.2f",$section->{'amount'}),
4242 product_code => 'N/A',
4244 push @newlines, $line;
4249 # after this, Calls Details is populated with all CDRs
4250 foreach my $newsection ( @newsections ) {
4251 if(!$newsection->{'post_total'}) { # this means Calls Details
4252 foreach my $line ( @lines ) {
4253 next unless (scalar(@{$line->{'ext_description'}}) &&
4254 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4256 my @extdesc = @{$line->{'ext_description'}};
4258 foreach my $extdesc ( @extdesc ) {
4259 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4260 push @newextdesc, $extdesc;
4262 $line->{'ext_description'} = \@newextdesc;
4263 $line->{'section'} = $newsection;
4264 push @newlines, $line;
4269 return(\@newsections, \@newlines);
4272 return(\@sections, \@lines);
4279 #my @display = scalar(@_)
4281 # : qw( _items_previous _items_pkg );
4282 # #: qw( _items_pkg );
4283 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4284 my @display = qw( _items_previous _items_pkg );
4287 foreach my $display ( @display ) {
4288 push @b, $self->$display(@_);
4293 sub _items_previous {
4295 my $cust_main = $self->cust_main;
4296 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4298 foreach ( @pr_cust_bill ) {
4299 my $date = $conf->exists('invoice_show_prior_due_date')
4300 ? 'due '. $_->due_date2str($date_format)
4301 : time2str($date_format, $_->_date);
4303 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4304 #'pkgpart' => 'N/A',
4306 'amount' => sprintf("%.2f", $_->owed),
4312 # 'description' => 'Previous Balance',
4313 # #'pkgpart' => 'N/A',
4314 # 'pkgnum' => 'N/A',
4315 # 'amount' => sprintf("%10.2f", $pr_total ),
4316 # 'ext_description' => [ map {
4317 # "Invoice ". $_->invnum.
4318 # " (". time2str("%x",$_->_date). ") ".
4319 # sprintf("%10.2f", $_->owed)
4320 # } @pr_cust_bill ],
4329 warn "$me _items_pkg searching for all package line items\n"
4332 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4334 warn "$me _items_pkg filtering line items\n"
4336 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4338 if ($options{section} && $options{section}->{condensed}) {
4340 warn "$me _items_pkg condensing section\n"
4344 local $Storable::canonical = 1;
4345 foreach ( @items ) {
4347 delete $item->{ref};
4348 delete $item->{ext_description};
4349 my $key = freeze($item);
4350 $itemshash{$key} ||= 0;
4351 $itemshash{$key} ++; # += $item->{quantity};
4353 @items = sort { $a->{description} cmp $b->{description} }
4354 map { my $i = thaw($_);
4355 $i->{quantity} = $itemshash{$_};
4357 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4363 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4370 return 0 unless $a->itemdesc cmp $b->itemdesc;
4371 return -1 if $b->itemdesc eq 'Tax';
4372 return 1 if $a->itemdesc eq 'Tax';
4373 return -1 if $b->itemdesc eq 'Other surcharges';
4374 return 1 if $a->itemdesc eq 'Other surcharges';
4375 $a->itemdesc cmp $b->itemdesc;
4380 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4381 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4384 sub _items_cust_bill_pkg {
4386 my $cust_bill_pkgs = shift;
4389 my $format = $opt{format} || '';
4390 my $escape_function = $opt{escape_function} || sub { shift };
4391 my $format_function = $opt{format_function} || '';
4392 my $unsquelched = $opt{unsquelched} || '';
4393 my $section = $opt{section}->{description} if $opt{section};
4394 my $summary_page = $opt{summary_page} || '';
4395 my $multilocation = $opt{multilocation} || '';
4396 my $multisection = $opt{multisection} || '';
4397 my $discount_show_always = 0;
4400 my ($s, $r, $u) = ( undef, undef, undef );
4401 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4404 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4407 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4408 && $conf->exists('discount-show-always'));
4410 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4411 if ( $_ && !$cust_bill_pkg->hidden ) {
4412 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4413 $_->{amount} =~ s/^\-0\.00$/0.00/;
4414 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4416 unless ( $_->{amount} == 0 && !$discount_show_always );
4421 foreach my $display ( grep { defined($section)
4422 ? $_->section eq $section
4425 #grep { !$_->summary || !$summary_page } # bunk!
4426 grep { !$_->summary || $multisection }
4427 $cust_bill_pkg->cust_bill_pkg_display
4431 warn "$me _items_cust_bill_pkg considering display item $display\n"
4434 my $type = $display->type;
4436 my $desc = $cust_bill_pkg->desc;
4437 $desc = substr($desc, 0, 50). '...'
4438 if $format eq 'latex' && length($desc) > 50;
4440 my %details_opt = ( 'format' => $format,
4441 'escape_function' => $escape_function,
4442 'format_function' => $format_function,
4445 if ( $cust_bill_pkg->pkgnum > 0 ) {
4447 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4450 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4452 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4454 warn "$me _items_cust_bill_pkg adding setup\n"
4457 my $description = $desc;
4458 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4461 unless ( $cust_pkg->part_pkg->hide_svc_detail
4462 || $cust_bill_pkg->hidden )
4465 push @d, map &{$escape_function}($_),
4466 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4467 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4469 if ( $multilocation ) {
4470 my $loc = $cust_pkg->location_label;
4471 $loc = substr($loc, 0, 50). '...'
4472 if $format eq 'latex' && length($loc) > 50;
4473 push @d, &{$escape_function}($loc);
4478 push @d, $cust_bill_pkg->details(%details_opt)
4479 if $cust_bill_pkg->recur == 0;
4481 if ( $cust_bill_pkg->hidden ) {
4482 $s->{amount} += $cust_bill_pkg->setup;
4483 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4484 push @{ $s->{ext_description} }, @d;
4487 description => $description,
4488 #pkgpart => $part_pkg->pkgpart,
4489 pkgnum => $cust_bill_pkg->pkgnum,
4490 amount => $cust_bill_pkg->setup,
4491 unit_amount => $cust_bill_pkg->unitsetup,
4492 quantity => $cust_bill_pkg->quantity,
4493 ext_description => \@d,
4499 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4500 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4501 ( !$type || $type eq 'R' || $type eq 'U' )
4505 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4508 my $is_summary = $display->summary;
4509 my $description = ($is_summary && $type && $type eq 'U')
4510 ? "Usage charges" : $desc;
4512 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4513 " - ". time2str($date_format, $cust_bill_pkg->edate).
4515 unless $conf->exists('disable_line_item_date_ranges');
4519 #at least until cust_bill_pkg has "past" ranges in addition to
4520 #the "future" sdate/edate ones... see #3032
4521 my @dates = ( $self->_date );
4522 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4523 push @dates, $prev->sdate if $prev;
4524 push @dates, undef if !$prev;
4526 unless ( $cust_pkg->part_pkg->hide_svc_detail
4527 || $cust_bill_pkg->itemdesc
4528 || $cust_bill_pkg->hidden
4529 || $is_summary && $type && $type eq 'U' )
4532 warn "$me _items_cust_bill_pkg adding service details\n"
4535 push @d, map &{$escape_function}($_),
4536 $cust_pkg->h_labels_short(@dates, 'I')
4537 #$cust_bill_pkg->edate,
4538 #$cust_bill_pkg->sdate)
4539 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4541 warn "$me _items_cust_bill_pkg done adding service details\n"
4544 if ( $multilocation ) {
4545 my $loc = $cust_pkg->location_label;
4546 $loc = substr($loc, 0, 50). '...'
4547 if $format eq 'latex' && length($loc) > 50;
4548 push @d, &{$escape_function}($loc);
4553 warn "$me _items_cust_bill_pkg adding details\n"
4556 push @d, $cust_bill_pkg->details(%details_opt)
4557 unless $is_summary; # || ($type && $type eq 'R');
4559 warn "$me _items_cust_bill_pkg calculating amount\n"
4564 $amount = $cust_bill_pkg->recur;
4565 }elsif($type eq 'R') {
4566 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4567 }elsif($type eq 'U') {
4568 $amount = $cust_bill_pkg->usage;
4571 if ( !$type || $type eq 'R' ) {
4573 warn "$me _items_cust_bill_pkg adding recur\n"
4576 if ( $cust_bill_pkg->hidden ) {
4577 $r->{amount} += $amount;
4578 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4579 push @{ $r->{ext_description} }, @d;
4582 description => $description,
4583 #pkgpart => $part_pkg->pkgpart,
4584 pkgnum => $cust_bill_pkg->pkgnum,
4586 unit_amount => $cust_bill_pkg->unitrecur,
4587 quantity => $cust_bill_pkg->quantity,
4588 ext_description => \@d,
4592 } else { # $type eq 'U'
4594 warn "$me _items_cust_bill_pkg adding usage\n"
4597 if ( $cust_bill_pkg->hidden ) {
4598 $u->{amount} += $amount;
4599 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4600 push @{ $u->{ext_description} }, @d;
4603 description => $description,
4604 #pkgpart => $part_pkg->pkgpart,
4605 pkgnum => $cust_bill_pkg->pkgnum,
4607 unit_amount => $cust_bill_pkg->unitrecur,
4608 quantity => $cust_bill_pkg->quantity,
4609 ext_description => \@d,
4615 } # recurring or usage with recurring charge
4617 } else { #pkgnum tax or one-shot line item (??)
4619 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4622 if ( $cust_bill_pkg->setup != 0 ) {
4624 'description' => $desc,
4625 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4628 if ( $cust_bill_pkg->recur != 0 ) {
4630 'description' => "$desc (".
4631 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4632 time2str($date_format, $cust_bill_pkg->edate). ')',
4633 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4643 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4646 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4648 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4649 $_->{amount} =~ s/^\-0\.00$/0.00/;
4650 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4652 unless ( $_->{amount} == 0 && !$discount_show_always );
4660 sub _items_credits {
4661 my( $self, %opt ) = @_;
4662 my $trim_len = $opt{'trim_len'} || 60;
4666 foreach ( $self->cust_credited ) {
4668 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4670 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4671 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4672 $reason = " ($reason) " if $reason;
4675 #'description' => 'Credit ref\#'. $_->crednum.
4676 # " (". time2str("%x",$_->cust_credit->_date) .")".
4678 'description' => 'Credit applied '.
4679 time2str($date_format,$_->cust_credit->_date). $reason,
4680 'amount' => sprintf("%.2f",$_->amount),
4688 sub _items_payments {
4692 #get & print payments
4693 foreach ( $self->cust_bill_pay ) {
4695 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4698 'description' => "Payment received ".
4699 time2str($date_format,$_->cust_pay->_date ),
4700 'amount' => sprintf("%.2f", $_->amount )
4708 =item call_details [ OPTION => VALUE ... ]
4710 Returns an array of CSV strings representing the call details for this invoice
4711 The only option available is the boolean prepend_billed_number
4716 my ($self, %opt) = @_;
4718 my $format_function = sub { shift };
4720 if ($opt{prepend_billed_number}) {
4721 $format_function = sub {
4725 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4730 my @details = map { $_->details( 'format_function' => $format_function,
4731 'escape_function' => sub{ return() },
4735 $self->cust_bill_pkg;
4736 my $header = $details[0];
4737 ( $header, grep { $_ ne $header } @details );
4747 =item process_reprint
4751 sub process_reprint {
4752 process_re_X('print', @_);
4755 =item process_reemail
4759 sub process_reemail {
4760 process_re_X('email', @_);
4768 process_re_X('fax', @_);
4776 process_re_X('ftp', @_);
4783 sub process_respool {
4784 process_re_X('spool', @_);
4787 use Storable qw(thaw);
4791 my( $method, $job ) = ( shift, shift );
4792 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4794 my $param = thaw(decode_base64(shift));
4795 warn Dumper($param) if $DEBUG;
4806 my($method, $job, %param ) = @_;
4808 warn "re_X $method for job $job with param:\n".
4809 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4812 #some false laziness w/search/cust_bill.html
4814 my $orderby = 'ORDER BY cust_bill._date';
4816 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4818 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4820 my @cust_bill = qsearch( {
4821 #'select' => "cust_bill.*",
4822 'table' => 'cust_bill',
4823 'addl_from' => $addl_from,
4825 'extra_sql' => $extra_sql,
4826 'order_by' => $orderby,
4830 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4832 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4835 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4836 foreach my $cust_bill ( @cust_bill ) {
4837 $cust_bill->$method();
4839 if ( $job ) { #progressbar foo
4841 if ( time - $min_sec > $last ) {
4842 my $error = $job->update_statustext(
4843 int( 100 * $num / scalar(@cust_bill) )
4845 die $error if $error;
4856 =head1 CLASS METHODS
4862 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4867 my ($class, $start, $end) = @_;
4869 $class->paid_sql($start, $end). ' - '.
4870 $class->credited_sql($start, $end);
4875 Returns an SQL fragment to retreive the net amount (charged minus credited).
4880 my ($class, $start, $end) = @_;
4881 'charged - '. $class->credited_sql($start, $end);
4886 Returns an SQL fragment to retreive the amount paid against this invoice.
4891 my ($class, $start, $end) = @_;
4892 $start &&= "AND cust_bill_pay._date <= $start";
4893 $end &&= "AND cust_bill_pay._date > $end";
4894 $start = '' unless defined($start);
4895 $end = '' unless defined($end);
4896 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4897 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4902 Returns an SQL fragment to retreive the amount credited against this invoice.
4907 my ($class, $start, $end) = @_;
4908 $start &&= "AND cust_credit_bill._date <= $start";
4909 $end &&= "AND cust_credit_bill._date > $end";
4910 $start = '' unless defined($start);
4911 $end = '' unless defined($end);
4912 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4913 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4918 Returns an SQL fragment to retrieve the due date of an invoice.
4919 Currently only supported on PostgreSQL.
4927 cust_bill.invoice_terms,
4928 cust_main.invoice_terms,
4929 \''.($conf->config('invoice_default_terms') || '').'\'
4930 ), E\'Net (\\\\d+)\'
4932 ) * 86400 + cust_bill._date'
4935 =item search_sql_where HASHREF
4937 Class method which returns an SQL WHERE fragment to search for parameters
4938 specified in HASHREF. Valid parameters are
4944 List reference of start date, end date, as UNIX timestamps.
4954 List reference of charged limits (exclusive).
4958 List reference of charged limits (exclusive).
4962 flag, return open invoices only
4966 flag, return net invoices only
4970 =item newest_percust
4974 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4978 sub search_sql_where {
4979 my($class, $param) = @_;
4981 warn "$me search_sql_where called with params: \n".
4982 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4988 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4989 push @search, "cust_main.agentnum = $1";
4993 if ( $param->{_date} ) {
4994 my($beginning, $ending) = @{$param->{_date}};
4996 push @search, "cust_bill._date >= $beginning",
4997 "cust_bill._date < $ending";
5001 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5002 push @search, "cust_bill.invnum >= $1";
5004 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5005 push @search, "cust_bill.invnum <= $1";
5009 if ( $param->{charged} ) {
5010 my @charged = ref($param->{charged})
5011 ? @{ $param->{charged} }
5012 : ($param->{charged});
5014 push @search, map { s/^charged/cust_bill.charged/; $_; }
5018 my $owed_sql = FS::cust_bill->owed_sql;
5021 if ( $param->{owed} ) {
5022 my @owed = ref($param->{owed})
5023 ? @{ $param->{owed} }
5025 push @search, map { s/^owed/$owed_sql/; $_; }
5030 push @search, "0 != $owed_sql"
5031 if $param->{'open'};
5032 push @search, '0 != '. FS::cust_bill->net_sql
5036 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5037 if $param->{'days'};
5040 if ( $param->{'newest_percust'} ) {
5042 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5043 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5045 my @newest_where = map { my $x = $_;
5046 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5049 grep ! /^cust_main./, @search;
5050 my $newest_where = scalar(@newest_where)
5051 ? ' AND '. join(' AND ', @newest_where)
5055 push @search, "cust_bill._date = (
5056 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5057 WHERE newest_cust_bill.custnum = cust_bill.custnum
5063 #agent virtualization
5064 my $curuser = $FS::CurrentUser::CurrentUser;
5065 if ( $curuser->username eq 'fs_queue'
5066 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5068 my $newuser = qsearchs('access_user', {
5069 'username' => $username,
5073 $curuser = $newuser;
5075 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5078 push @search, $curuser->agentnums_sql;
5080 join(' AND ', @search );
5092 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5093 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base