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 my %newline_tokens = ( 'latex' => '\\\\',
2440 my $newline_token = $newline_tokens{$format};
2442 warn "$me generating template variables\n"
2445 # generate template variables
2448 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2452 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2458 $returnaddress = join("\n",
2459 $conf->config_orbase("invoice_${format}returnaddress", $template)
2462 } elsif ( grep /\S/,
2463 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2465 my $convert_map = $convert_maps{$format}{'returnaddress'};
2468 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2473 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2475 my $convert_map = $convert_maps{$format}{'returnaddress'};
2476 $returnaddress = join( "\n", &$convert_map(
2477 map { s/( {2,})/'~' x length($1)/eg;
2481 ( $conf->config('company_name', $self->cust_main->agentnum),
2482 $conf->config('company_address', $self->cust_main->agentnum),
2489 my $warning = "Couldn't find a return address; ".
2490 "do you need to set the company_address configuration value?";
2492 $returnaddress = $nbsp;
2493 #$returnaddress = $warning;
2497 warn "$me generating invoice data\n"
2500 my $agentnum = $self->cust_main->agentnum;
2502 my %invoice_data = (
2505 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2506 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2507 'returnaddress' => $returnaddress,
2508 'agent' => &$escape_function($cust_main->agent->agent),
2511 'invnum' => $self->invnum,
2512 'date' => time2str($date_format, $self->_date),
2513 'today' => time2str($date_format_long, $today),
2514 'terms' => $self->terms,
2515 'template' => $template, #params{'template'},
2516 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2517 'current_charges' => sprintf("%.2f", $self->charged),
2518 'duedate' => $self->due_date2str($rdate_format), #date_format?
2521 'custnum' => $cust_main->display_custnum,
2522 'agent_custid' => &$escape_function($cust_main->agent_custid),
2523 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2524 payname company address1 address2 city state zip fax
2528 'ship_enable' => $conf->exists('invoice-ship_address'),
2529 'unitprices' => $conf->exists('invoice-unitprice'),
2530 'smallernotes' => $conf->exists('invoice-smallernotes'),
2531 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2532 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2534 #layout info -- would be fancy to calc some of this and bury the template
2536 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2537 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2538 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2539 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2540 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2541 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2542 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2543 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2544 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2545 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2547 # better hang on to conf_dir for a while (for old templates)
2548 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2550 #these are only used when doing paged plaintext
2556 my $min_sdate = 999999999999;
2558 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2559 next unless $cust_bill_pkg->pkgnum > 0;
2560 $min_sdate = $cust_bill_pkg->sdate
2561 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2562 $max_edate = $cust_bill_pkg->edate
2563 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2566 $invoice_data{'bill_period'} = '';
2567 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2568 . " to " . time2str('%e %h', $max_edate)
2569 if ($max_edate != 0 && $min_sdate != 999999999999);
2571 $invoice_data{finance_section} = '';
2572 if ( $conf->config('finance_pkgclass') ) {
2574 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2575 $invoice_data{finance_section} = $pkg_class->categoryname;
2577 $invoice_data{finance_amount} = '0.00';
2578 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2580 my $countrydefault = $conf->config('countrydefault') || 'US';
2581 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2582 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2583 my $method = $prefix.$_;
2584 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2586 $invoice_data{'ship_country'} = ''
2587 if ( $invoice_data{'ship_country'} eq $countrydefault );
2589 $invoice_data{'cid'} = $params{'cid'}
2592 if ( $cust_main->country eq $countrydefault ) {
2593 $invoice_data{'country'} = '';
2595 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2599 $invoice_data{'address'} = \@address;
2601 $cust_main->payname.
2602 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2603 ? " (P.O. #". $cust_main->payinfo. ")"
2607 push @address, $cust_main->company
2608 if $cust_main->company;
2609 push @address, $cust_main->address1;
2610 push @address, $cust_main->address2
2611 if $cust_main->address2;
2613 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2614 push @address, $invoice_data{'country'}
2615 if $invoice_data{'country'};
2617 while (scalar(@address) < 5);
2619 $invoice_data{'logo_file'} = $params{'logo_file'}
2620 if $params{'logo_file'};
2621 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2622 if $params{'barcode_file'};
2623 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2624 if $params{'barcode_img'};
2625 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2626 if $params{'barcode_cid'};
2628 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2629 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2630 #my $balance_due = $self->owed + $pr_total - $cr_total;
2631 my $balance_due = $self->owed + $pr_total;
2632 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2633 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2634 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2635 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2637 my $summarypage = '';
2638 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2641 $invoice_data{'summarypage'} = $summarypage;
2643 warn "$me substituting variables in notes, footer, smallfooter\n"
2646 foreach my $include (qw( notes footer smallfooter coupon )) {
2648 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2651 if ( $conf->exists($inc_file, $agentnum)
2652 && length( $conf->config($inc_file, $agentnum) ) ) {
2654 @inc_src = $conf->config($inc_file, $agentnum);
2658 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2660 my $convert_map = $convert_maps{$format}{$include};
2662 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2663 s/--\@\]/$delimiters{$format}[1]/g;
2666 &$convert_map( $conf->config($inc_file, $agentnum) );
2670 my $inc_tt = new Text::Template (
2672 SOURCE => [ map "$_\n", @inc_src ],
2673 DELIMITERS => $delimiters{$format},
2674 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2676 unless ( $inc_tt->compile() ) {
2677 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2678 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2682 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2684 $invoice_data{$include} =~ s/\n+$//
2685 if ($format eq 'latex');
2688 $invoice_data{'po_line'} =
2689 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2690 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2693 my %money_chars = ( 'latex' => '',
2694 'html' => $conf->config('money_char') || '$',
2697 my $money_char = $money_chars{$format};
2699 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2700 'html' => $conf->config('money_char') || '$',
2703 my $other_money_char = $other_money_chars{$format};
2704 $invoice_data{'dollar'} = $other_money_char;
2706 my @detail_items = ();
2707 my @total_items = ();
2711 $invoice_data{'detail_items'} = \@detail_items;
2712 $invoice_data{'total_items'} = \@total_items;
2713 $invoice_data{'buf'} = \@buf;
2714 $invoice_data{'sections'} = \@sections;
2716 warn "$me generating sections\n"
2719 my $previous_section = { 'description' => 'Previous Charges',
2720 'subtotal' => $other_money_char.
2721 sprintf('%.2f', $pr_total),
2722 'summarized' => $summarypage ? 'Y' : '',
2724 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2725 join(' / ', map { $cust_main->balance_date_range(@$_) }
2726 $self->_prior_month30s
2728 if $conf->exists('invoice_include_aging');
2731 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2732 'subtotal' => $taxtotal, # adjusted below
2733 'summarized' => $summarypage ? 'Y' : '',
2735 my $tax_weight = _pkg_category($tax_section->{description})
2736 ? _pkg_category($tax_section->{description})->weight
2738 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2739 $tax_section->{'sort_weight'} = $tax_weight;
2742 my $adjusttotal = 0;
2743 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2744 'subtotal' => 0, # adjusted below
2745 'summarized' => $summarypage ? 'Y' : '',
2747 my $adjust_weight = _pkg_category($adjust_section->{description})
2748 ? _pkg_category($adjust_section->{description})->weight
2750 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2751 $adjust_section->{'sort_weight'} = $adjust_weight;
2753 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2754 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2755 $invoice_data{'multisection'} = $multisection;
2756 my $late_sections = [];
2757 my $extra_sections = [];
2758 my $extra_lines = ();
2759 if ( $multisection ) {
2760 ($extra_sections, $extra_lines) =
2761 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2762 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2764 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2766 push @detail_items, @$extra_lines if $extra_lines;
2768 $self->_items_sections( $late_sections, # this could stand a refactor
2770 $escape_function_nonbsp,
2774 if ($conf->exists('svc_phone_sections')) {
2775 my ($phone_sections, $phone_lines) =
2776 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2777 push @{$late_sections}, @$phone_sections;
2778 push @detail_items, @$phone_lines;
2781 push @sections, { 'description' => '', 'subtotal' => '' };
2784 unless ( $conf->exists('disable_previous_balance')
2785 || $conf->exists('previous_balance-summary_only')
2789 warn "$me adding previous balances\n"
2792 foreach my $line_item ( $self->_items_previous ) {
2795 ext_description => [],
2797 $detail->{'ref'} = $line_item->{'pkgnum'};
2798 $detail->{'quantity'} = 1;
2799 $detail->{'section'} = $previous_section;
2800 $detail->{'description'} = &$escape_function($line_item->{'description'});
2801 if ( exists $line_item->{'ext_description'} ) {
2802 @{$detail->{'ext_description'}} = map {
2803 &$escape_function($_);
2804 } @{$line_item->{'ext_description'}};
2806 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2807 $line_item->{'amount'};
2808 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2810 push @detail_items, $detail;
2811 push @buf, [ $detail->{'description'},
2812 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2818 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2819 push @buf, ['','-----------'];
2820 push @buf, [ 'Total Previous Balance',
2821 $money_char. sprintf("%10.2f", $pr_total) ];
2825 if ( $conf->exists('svc_phone-did-summary') ) {
2826 warn "$me adding DID summary\n"
2829 my ($didsummary,$minutes) = $self->_did_summary;
2830 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2832 { 'description' => $didsummary_desc,
2833 'ext_description' => [ $didsummary, $minutes ],
2838 foreach my $section (@sections, @$late_sections) {
2840 warn "$me adding section \n". Dumper($section)
2843 # begin some normalization
2844 $section->{'subtotal'} = $section->{'amount'}
2846 && !exists($section->{subtotal})
2847 && exists($section->{amount});
2849 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2850 if ( $invoice_data{finance_section} &&
2851 $section->{'description'} eq $invoice_data{finance_section} );
2853 $section->{'subtotal'} = $other_money_char.
2854 sprintf('%.2f', $section->{'subtotal'})
2857 # continue some normalization
2858 $section->{'amount'} = $section->{'subtotal'}
2862 if ( $section->{'description'} ) {
2863 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2868 warn "$me setting options\n"
2871 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2873 $options{'section'} = $section if $multisection;
2874 $options{'format'} = $format;
2875 $options{'escape_function'} = $escape_function;
2876 $options{'format_function'} = sub { () } unless $unsquelched;
2877 $options{'unsquelched'} = $unsquelched;
2878 $options{'summary_page'} = $summarypage;
2879 $options{'skip_usage'} =
2880 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2881 $options{'multilocation'} = $multilocation;
2882 $options{'multisection'} = $multisection;
2884 warn "$me searching for line items\n"
2887 foreach my $line_item ( $self->_items_pkg(%options) ) {
2889 warn "$me adding line item $line_item\n"
2893 ext_description => [],
2895 $detail->{'ref'} = $line_item->{'pkgnum'};
2896 $detail->{'quantity'} = $line_item->{'quantity'};
2897 $detail->{'section'} = $section;
2898 $detail->{'description'} = &$escape_function($line_item->{'description'});
2899 if ( exists $line_item->{'ext_description'} ) {
2900 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2902 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2903 $line_item->{'amount'};
2904 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2905 $line_item->{'unit_amount'};
2906 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2908 push @detail_items, $detail;
2909 push @buf, ( [ $detail->{'description'},
2910 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2912 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2916 if ( $section->{'description'} ) {
2917 push @buf, ( ['','-----------'],
2918 [ $section->{'description'}. ' sub-total',
2919 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2928 $invoice_data{current_less_finance} =
2929 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2931 if ( $multisection && !$conf->exists('disable_previous_balance')
2932 || $conf->exists('previous_balance-summary_only') )
2934 unshift @sections, $previous_section if $pr_total;
2937 warn "$me adding taxes\n"
2940 foreach my $tax ( $self->_items_tax ) {
2942 $taxtotal += $tax->{'amount'};
2944 my $description = &$escape_function( $tax->{'description'} );
2945 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2947 if ( $multisection ) {
2949 my $money = $old_latex ? '' : $money_char;
2950 push @detail_items, {
2951 ext_description => [],
2954 description => $description,
2955 amount => $money. $amount,
2957 section => $tax_section,
2962 push @total_items, {
2963 'total_item' => $description,
2964 'total_amount' => $other_money_char. $amount,
2969 push @buf,[ $description,
2970 $money_char. $amount,
2977 $total->{'total_item'} = 'Sub-total';
2978 $total->{'total_amount'} =
2979 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2981 if ( $multisection ) {
2982 $tax_section->{'subtotal'} = $other_money_char.
2983 sprintf('%.2f', $taxtotal);
2984 $tax_section->{'pretotal'} = 'New charges sub-total '.
2985 $total->{'total_amount'};
2986 push @sections, $tax_section if $taxtotal;
2988 unshift @total_items, $total;
2991 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2993 push @buf,['','-----------'];
2994 push @buf,[( $conf->exists('disable_previous_balance')
2996 : 'Total New Charges'
2998 $money_char. sprintf("%10.2f",$self->charged) ];
3004 $item = $conf->config('previous_balance-exclude_from_total')
3005 || 'Total New Charges'
3006 if $conf->exists('previous_balance-exclude_from_total');
3007 my $amount = $self->charged +
3008 ( $conf->exists('disable_previous_balance') ||
3009 $conf->exists('previous_balance-exclude_from_total')
3013 $total->{'total_item'} = &$embolden_function($item);
3014 $total->{'total_amount'} =
3015 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3016 if ( $multisection ) {
3017 if ( $adjust_section->{'sort_weight'} ) {
3018 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3019 sprintf("%.2f", ($self->billing_balance || 0) );
3021 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3022 sprintf('%.2f', $self->charged );
3025 push @total_items, $total;
3027 push @buf,['','-----------'];
3030 sprintf( '%10.2f', $amount )
3035 unless ( $conf->exists('disable_previous_balance') ) {
3036 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3039 my $credittotal = 0;
3040 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3043 $total->{'total_item'} = &$escape_function($credit->{'description'});
3044 $credittotal += $credit->{'amount'};
3045 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3046 $adjusttotal += $credit->{'amount'};
3047 if ( $multisection ) {
3048 my $money = $old_latex ? '' : $money_char;
3049 push @detail_items, {
3050 ext_description => [],
3053 description => &$escape_function($credit->{'description'}),
3054 amount => $money. $credit->{'amount'},
3056 section => $adjust_section,
3059 push @total_items, $total;
3063 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3066 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3067 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3071 my $paymenttotal = 0;
3072 foreach my $payment ( $self->_items_payments ) {
3074 $total->{'total_item'} = &$escape_function($payment->{'description'});
3075 $paymenttotal += $payment->{'amount'};
3076 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3077 $adjusttotal += $payment->{'amount'};
3078 if ( $multisection ) {
3079 my $money = $old_latex ? '' : $money_char;
3080 push @detail_items, {
3081 ext_description => [],
3084 description => &$escape_function($payment->{'description'}),
3085 amount => $money. $payment->{'amount'},
3087 section => $adjust_section,
3090 push @total_items, $total;
3092 push @buf, [ $payment->{'description'},
3093 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3096 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3098 if ( $multisection ) {
3099 $adjust_section->{'subtotal'} = $other_money_char.
3100 sprintf('%.2f', $adjusttotal);
3101 push @sections, $adjust_section
3102 unless $adjust_section->{sort_weight};
3107 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3108 $total->{'total_amount'} =
3109 &$embolden_function(
3110 $other_money_char. sprintf('%.2f', $summarypage
3112 $self->billing_balance
3113 : $self->owed + $pr_total
3116 if ( $multisection && !$adjust_section->{sort_weight} ) {
3117 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3118 $total->{'total_amount'};
3120 push @total_items, $total;
3122 push @buf,['','-----------'];
3123 push @buf,[$self->balance_due_msg, $money_char.
3124 sprintf("%10.2f", $balance_due ) ];
3127 if ( $conf->exists('previous_balance-show_credit')
3128 and $cust_main->balance < 0 ) {
3129 my $credit_total = {
3130 'total_item' => &$embolden_function($self->credit_balance_msg),
3131 'total_amount' => &$embolden_function(
3132 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3135 if ( $multisection ) {
3136 $adjust_section->{'posttotal'} .= $newline_token .
3137 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3140 push @total_items, $credit_total;
3142 push @buf,['','-----------'];
3143 push @buf,[$self->credit_balance_msg, $money_char.
3144 sprintf("%10.2f", -$cust_main->balance ) ];
3148 if ( $multisection ) {
3149 if ($conf->exists('svc_phone_sections')) {
3151 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3152 $total->{'total_amount'} =
3153 &$embolden_function(
3154 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3156 my $last_section = pop @sections;
3157 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3158 $total->{'total_amount'};
3159 push @sections, $last_section;
3161 push @sections, @$late_sections
3165 my @includelist = ();
3166 push @includelist, 'summary' if $summarypage;
3167 foreach my $include ( @includelist ) {
3169 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3172 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3174 @inc_src = $conf->config($inc_file, $agentnum);
3178 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3180 my $convert_map = $convert_maps{$format}{$include};
3182 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3183 s/--\@\]/$delimiters{$format}[1]/g;
3186 &$convert_map( $conf->config($inc_file, $agentnum) );
3190 my $inc_tt = new Text::Template (
3192 SOURCE => [ map "$_\n", @inc_src ],
3193 DELIMITERS => $delimiters{$format},
3194 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3196 unless ( $inc_tt->compile() ) {
3197 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3198 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3202 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3204 $invoice_data{$include} =~ s/\n+$//
3205 if ($format eq 'latex');
3210 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3211 /invoice_lines\((\d*)\)/;
3212 $invoice_lines += $1 || scalar(@buf);
3215 die "no invoice_lines() functions in template?"
3216 if ( $format eq 'template' && !$wasfunc );
3218 if ($format eq 'template') {
3220 if ( $invoice_lines ) {
3221 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3222 $invoice_data{'total_pages'}++
3223 if scalar(@buf) % $invoice_lines;
3226 #setup subroutine for the template
3227 sub FS::cust_bill::_template::invoice_lines {
3228 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3230 scalar(@FS::cust_bill::_template::buf)
3231 ? shift @FS::cust_bill::_template::buf
3240 push @collect, split("\n",
3241 $text_template->fill_in( HASH => \%invoice_data,
3242 PACKAGE => 'FS::cust_bill::_template'
3245 $FS::cust_bill::_template::page++;
3247 map "$_\n", @collect;
3249 warn "filling in template for invoice ". $self->invnum. "\n"
3251 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3254 $text_template->fill_in(HASH => \%invoice_data);
3258 # helper routine for generating date ranges
3259 sub _prior_month30s {
3262 [ 1, 2592000 ], # 0-30 days ago
3263 [ 2592000, 5184000 ], # 30-60 days ago
3264 [ 5184000, 7776000 ], # 60-90 days ago
3265 [ 7776000, 0 ], # 90+ days ago
3268 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3269 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3274 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3276 Returns an postscript invoice, as a scalar.
3278 Options can be passed as a hashref (recommended) or as a list of time, template
3279 and then any key/value pairs for any other options.
3281 I<time> an optional value used to control the printing of overdue messages. The
3282 default is now. It isn't the date of the invoice; that's the `_date' field.
3283 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3284 L<Time::Local> and L<Date::Parse> for conversion functions.
3286 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3293 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3294 my $ps = generate_ps($file);
3296 unlink($barcodefile);
3301 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3303 Returns an PDF invoice, as a scalar.
3305 Options can be passed as a hashref (recommended) or as a list of time, template
3306 and then any key/value pairs for any other options.
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)
3322 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3323 my $pdf = generate_pdf($file);
3325 unlink($barcodefile);
3330 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3332 Returns an HTML invoice, as a scalar.
3334 I<time> an optional value used to control the printing of overdue messages. The
3335 default is now. It isn't the date of the invoice; that's the `_date' field.
3336 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3337 L<Time::Local> and L<Date::Parse> for conversion functions.
3339 I<template>, if specified, is the name of a suffix for alternate invoices.
3341 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3343 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3344 when emailing the invoice as part of a multipart/related MIME email.
3352 %params = %{ shift() };
3354 $params{'time'} = shift;
3355 $params{'template'} = shift;
3356 $params{'cid'} = shift;
3359 $params{'format'} = 'html';
3361 $self->print_generic( %params );
3364 # quick subroutine for print_latex
3366 # There are ten characters that LaTeX treats as special characters, which
3367 # means that they do not simply typeset themselves:
3368 # # $ % & ~ _ ^ \ { }
3370 # TeX ignores blanks following an escaped character; if you want a blank (as
3371 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3375 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3376 $value =~ s/([<>])/\$$1\$/g;
3382 encode_entities($value);
3386 sub _html_escape_nbsp {
3387 my $value = _html_escape(shift);
3388 $value =~ s/ +/ /g;
3392 #utility methods for print_*
3394 sub _translate_old_latex_format {
3395 warn "_translate_old_latex_format called\n"
3402 if ( $line =~ /^%%Detail\s*$/ ) {
3404 push @template, q![@--!,
3405 q! foreach my $_tr_line (@detail_items) {!,
3406 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3407 q! $_tr_line->{'description'} .= !,
3408 q! "\\tabularnewline\n~~".!,
3409 q! join( "\\tabularnewline\n~~",!,
3410 q! @{$_tr_line->{'ext_description'}}!,
3414 while ( ( my $line_item_line = shift )
3415 !~ /^%%EndDetail\s*$/ ) {
3416 $line_item_line =~ s/'/\\'/g; # nice LTS
3417 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3418 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3419 push @template, " \$OUT .= '$line_item_line';";
3422 push @template, '}',
3425 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3427 push @template, '[@--',
3428 ' foreach my $_tr_line (@total_items) {';
3430 while ( ( my $total_item_line = shift )
3431 !~ /^%%EndTotalDetails\s*$/ ) {
3432 $total_item_line =~ s/'/\\'/g; # nice LTS
3433 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3434 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3435 push @template, " \$OUT .= '$total_item_line';";
3438 push @template, '}',
3442 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3443 push @template, $line;
3449 warn "$_\n" foreach @template;
3458 #check for an invoice-specific override
3459 return $self->invoice_terms if $self->invoice_terms;
3461 #check for a customer- specific override
3462 my $cust_main = $self->cust_main;
3463 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3465 #use configured default
3466 $conf->config('invoice_default_terms') || '';
3472 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3473 $duedate = $self->_date() + ( $1 * 86400 );
3480 $self->due_date ? time2str(shift, $self->due_date) : '';
3483 sub balance_due_msg {
3485 my $msg = 'Balance Due';
3486 return $msg unless $self->terms;
3487 if ( $self->due_date ) {
3488 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3489 } elsif ( $self->terms ) {
3490 $msg .= ' - '. $self->terms;
3495 sub balance_due_date {
3498 if ( $conf->exists('invoice_default_terms')
3499 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3500 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3505 sub credit_balance_msg { 'Credit Balance Remaining' }
3507 =item invnum_date_pretty
3509 Returns a string with the invoice number and date, for example:
3510 "Invoice #54 (3/20/2008)"
3514 sub invnum_date_pretty {
3516 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3521 Returns a string with the date, for example: "3/20/2008"
3527 time2str($date_format, $self->_date);
3530 use vars qw(%pkg_category_cache);
3531 sub _items_sections {
3534 my $summarypage = shift;
3536 my $extra_sections = shift;
3540 my %late_subtotal = ();
3543 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3546 my $usage = $cust_bill_pkg->usage;
3548 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3549 next if ( $display->summary && $summarypage );
3551 my $section = $display->section;
3552 my $type = $display->type;
3554 $not_tax{$section} = 1
3555 unless $cust_bill_pkg->pkgnum == 0;
3557 if ( $display->post_total && !$summarypage ) {
3558 if (! $type || $type eq 'S') {
3559 $late_subtotal{$section} += $cust_bill_pkg->setup
3560 if $cust_bill_pkg->setup != 0;
3564 $late_subtotal{$section} += $cust_bill_pkg->recur
3565 if $cust_bill_pkg->recur != 0;
3568 if ($type && $type eq 'R') {
3569 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3570 if $cust_bill_pkg->recur != 0;
3573 if ($type && $type eq 'U') {
3574 $late_subtotal{$section} += $usage
3575 unless scalar(@$extra_sections);
3580 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3582 if (! $type || $type eq 'S') {
3583 $subtotal{$section} += $cust_bill_pkg->setup
3584 if $cust_bill_pkg->setup != 0;
3588 $subtotal{$section} += $cust_bill_pkg->recur
3589 if $cust_bill_pkg->recur != 0;
3592 if ($type && $type eq 'R') {
3593 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3594 if $cust_bill_pkg->recur != 0;
3597 if ($type && $type eq 'U') {
3598 $subtotal{$section} += $usage
3599 unless scalar(@$extra_sections);
3608 %pkg_category_cache = ();
3610 push @$late, map { { 'description' => &{$escape}($_),
3611 'subtotal' => $late_subtotal{$_},
3613 'sort_weight' => ( _pkg_category($_)
3614 ? _pkg_category($_)->weight
3617 ((_pkg_category($_) && _pkg_category($_)->condense)
3618 ? $self->_condense_section($format)
3622 sort _sectionsort keys %late_subtotal;
3625 if ( $summarypage ) {
3626 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3627 map { $_->categoryname } qsearch('pkg_category', {});
3628 push @sections, '' if exists($subtotal{''});
3630 @sections = keys %subtotal;
3633 my @early = map { { 'description' => &{$escape}($_),
3634 'subtotal' => $subtotal{$_},
3635 'summarized' => $not_tax{$_} ? '' : 'Y',
3636 'tax_section' => $not_tax{$_} ? '' : 'Y',
3637 'sort_weight' => ( _pkg_category($_)
3638 ? _pkg_category($_)->weight
3641 ((_pkg_category($_) && _pkg_category($_)->condense)
3642 ? $self->_condense_section($format)
3647 push @early, @$extra_sections if $extra_sections;
3649 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3653 #helper subs for above
3656 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3660 my $categoryname = shift;
3661 $pkg_category_cache{$categoryname} ||=
3662 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3665 my %condensed_format = (
3666 'label' => [ qw( Description Qty Amount ) ],
3668 sub { shift->{description} },
3669 sub { shift->{quantity} },
3670 sub { my($href, %opt) = @_;
3671 ($opt{dollar} || ''). $href->{amount};
3674 'align' => [ qw( l r r ) ],
3675 'span' => [ qw( 5 1 1 ) ], # unitprices?
3676 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3679 sub _condense_section {
3680 my ( $self, $format ) = ( shift, shift );
3682 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3683 qw( description_generator
3686 total_line_generator
3691 sub _condensed_generator_defaults {
3692 my ( $self, $format ) = ( shift, shift );
3693 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3702 sub _condensed_header_generator {
3703 my ( $self, $format ) = ( shift, shift );
3705 my ( $f, $prefix, $suffix, $separator, $column ) =
3706 _condensed_generator_defaults($format);
3708 if ($format eq 'latex') {
3709 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3710 $suffix = "\\\\\n\\hline";
3713 sub { my ($d,$a,$s,$w) = @_;
3714 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3716 } elsif ( $format eq 'html' ) {
3717 $prefix = '<th></th>';
3721 sub { my ($d,$a,$s,$w) = @_;
3722 return qq!<th align="$html_align{$a}">$d</th>!;
3730 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3732 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3735 $prefix. join($separator, @result). $suffix;
3740 sub _condensed_description_generator {
3741 my ( $self, $format ) = ( shift, shift );
3743 my ( $f, $prefix, $suffix, $separator, $column ) =
3744 _condensed_generator_defaults($format);
3746 my $money_char = '$';
3747 if ($format eq 'latex') {
3748 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3750 $separator = " & \n";
3752 sub { my ($d,$a,$s,$w) = @_;
3753 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3755 $money_char = '\\dollar';
3756 }elsif ( $format eq 'html' ) {
3757 $prefix = '"><td align="center"></td>';
3761 sub { my ($d,$a,$s,$w) = @_;
3762 return qq!<td align="$html_align{$a}">$d</td>!;
3764 #$money_char = $conf->config('money_char') || '$';
3765 $money_char = ''; # this is madness
3773 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3775 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3777 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3778 map { $f->{$_}->[$i] } qw(align span width)
3782 $prefix. join( $separator, @result ). $suffix;
3787 sub _condensed_total_generator {
3788 my ( $self, $format ) = ( shift, shift );
3790 my ( $f, $prefix, $suffix, $separator, $column ) =
3791 _condensed_generator_defaults($format);
3794 if ($format eq 'latex') {
3797 $separator = " & \n";
3799 sub { my ($d,$a,$s,$w) = @_;
3800 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3802 }elsif ( $format eq 'html' ) {
3806 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3808 sub { my ($d,$a,$s,$w) = @_;
3809 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3818 # my $r = &{$f->{fields}->[$i]}(@args);
3819 # $r .= ' Total' unless $i;
3821 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3823 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3824 map { $f->{$_}->[$i] } qw(align span width)
3828 $prefix. join( $separator, @result ). $suffix;
3833 =item total_line_generator FORMAT
3835 Returns a coderef used for generation of invoice total line items for this
3836 usage_class. FORMAT is either html or latex
3840 # should not be used: will have issues with hash element names (description vs
3841 # total_item and amount vs total_amount -- another array of functions?
3843 sub _condensed_total_line_generator {
3844 my ( $self, $format ) = ( shift, shift );
3846 my ( $f, $prefix, $suffix, $separator, $column ) =
3847 _condensed_generator_defaults($format);
3850 if ($format eq 'latex') {
3853 $separator = " & \n";
3855 sub { my ($d,$a,$s,$w) = @_;
3856 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3858 }elsif ( $format eq 'html' ) {
3862 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3864 sub { my ($d,$a,$s,$w) = @_;
3865 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3874 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3876 &{$column}( &{$f->{fields}->[$i]}(@args),
3877 map { $f->{$_}->[$i] } qw(align span width)
3881 $prefix. join( $separator, @result ). $suffix;
3886 #sub _items_extra_usage_sections {
3888 # my $escape = shift;
3890 # my %sections = ();
3892 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3893 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3895 # next unless $cust_bill_pkg->pkgnum > 0;
3897 # foreach my $section ( keys %usage_class ) {
3899 # my $usage = $cust_bill_pkg->usage($section);
3901 # next unless $usage && $usage > 0;
3903 # $sections{$section} ||= 0;
3904 # $sections{$section} += $usage;
3910 # map { { 'description' => &{$escape}($_),
3911 # 'subtotal' => $sections{$_},
3912 # 'summarized' => '',
3913 # 'tax_section' => '',
3916 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3920 sub _items_extra_usage_sections {
3929 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3930 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3931 next unless $cust_bill_pkg->pkgnum > 0;
3933 foreach my $classnum ( keys %usage_class ) {
3934 my $section = $usage_class{$classnum}->classname;
3935 $classnums{$section} = $classnum;
3937 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3938 my $amount = $detail->amount;
3939 next unless $amount && $amount > 0;
3941 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3942 $sections{$section}{amount} += $amount; #subtotal
3943 $sections{$section}{calls}++;
3944 $sections{$section}{duration} += $detail->duration;
3946 my $desc = $detail->regionname;
3947 my $description = $desc;
3948 $description = substr($desc, 0, 50). '...'
3949 if $format eq 'latex' && length($desc) > 50;
3951 $lines{$section}{$desc} ||= {
3952 description => &{$escape}($description),
3953 #pkgpart => $part_pkg->pkgpart,
3954 pkgnum => $cust_bill_pkg->pkgnum,
3959 #unit_amount => $cust_bill_pkg->unitrecur,
3960 quantity => $cust_bill_pkg->quantity,
3961 product_code => 'N/A',
3962 ext_description => [],
3965 $lines{$section}{$desc}{amount} += $amount;
3966 $lines{$section}{$desc}{calls}++;
3967 $lines{$section}{$desc}{duration} += $detail->duration;
3973 my %sectionmap = ();
3974 foreach (keys %sections) {
3975 my $usage_class = $usage_class{$classnums{$_}};
3976 $sectionmap{$_} = { 'description' => &{$escape}($_),
3977 'amount' => $sections{$_}{amount}, #subtotal
3978 'calls' => $sections{$_}{calls},
3979 'duration' => $sections{$_}{duration},
3981 'tax_section' => '',
3982 'sort_weight' => $usage_class->weight,
3983 ( $usage_class->format
3984 ? ( map { $_ => $usage_class->$_($format) }
3985 qw( description_generator header_generator total_generator total_line_generator )
3992 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3996 foreach my $section ( keys %lines ) {
3997 foreach my $line ( keys %{$lines{$section}} ) {
3998 my $l = $lines{$section}{$line};
3999 $l->{section} = $sectionmap{$section};
4000 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4001 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4006 return(\@sections, \@lines);
4012 my $end = $self->_date;
4013 my $start = $end - 2592000; # 30 days
4014 my $cust_main = $self->cust_main;
4015 my @pkgs = $cust_main->all_pkgs;
4016 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4019 foreach my $pkg ( @pkgs ) {
4020 my @h_cust_svc = $pkg->h_cust_svc($end);
4021 foreach my $h_cust_svc ( @h_cust_svc ) {
4022 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4023 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4025 my $inserted = $h_cust_svc->date_inserted;
4026 my $deleted = $h_cust_svc->date_deleted;
4027 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4029 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4031 # DID either activated or ported in; cannot be both for same DID simultaneously
4032 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4033 && (!$phone_inserted->lnp_status
4034 || $phone_inserted->lnp_status eq ''
4035 || $phone_inserted->lnp_status eq 'native')) {
4038 else { # this one not so clean, should probably move to (h_)svc_phone
4039 my $phone_portedin = qsearchs( 'h_svc_phone',
4040 { 'svcnum' => $h_cust_svc->svcnum,
4041 'lnp_status' => 'portedin' },
4042 FS::h_svc_phone->sql_h_searchs($end),
4044 $num_portedin++ if $phone_portedin;
4047 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4048 if($deleted >= $start && $deleted <= $end && $phone_deleted
4049 && (!$phone_deleted->lnp_status
4050 || $phone_deleted->lnp_status ne 'portingout')) {
4053 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4054 && $phone_deleted->lnp_status
4055 && $phone_deleted->lnp_status eq 'portingout') {
4059 # increment usage minutes
4060 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4061 foreach my $cdr ( @cdrs ) {
4062 $minutes += $cdr->billsec/60;
4065 # don't look at this service again
4066 push @seen, $h_cust_svc->svcnum;
4070 $minutes = sprintf("%d", $minutes);
4071 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4072 . "$num_deactivated Ported-Out: $num_portedout ",
4073 "Total Minutes: $minutes");
4076 sub _items_svc_phone_sections {
4085 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4086 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4088 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4089 next unless $cust_bill_pkg->pkgnum > 0;
4091 my @header = $cust_bill_pkg->details_header;
4092 next unless scalar(@header);
4094 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4096 my $phonenum = $detail->phonenum;
4097 next unless $phonenum;
4099 my $amount = $detail->amount;
4100 next unless $amount && $amount > 0;
4102 $sections{$phonenum} ||= { 'amount' => 0,
4105 'sort_weight' => -1,
4106 'phonenum' => $phonenum,
4108 $sections{$phonenum}{amount} += $amount; #subtotal
4109 $sections{$phonenum}{calls}++;
4110 $sections{$phonenum}{duration} += $detail->duration;
4112 my $desc = $detail->regionname;
4113 my $description = $desc;
4114 $description = substr($desc, 0, 50). '...'
4115 if $format eq 'latex' && length($desc) > 50;
4117 $lines{$phonenum}{$desc} ||= {
4118 description => &{$escape}($description),
4119 #pkgpart => $part_pkg->pkgpart,
4127 product_code => 'N/A',
4128 ext_description => [],
4131 $lines{$phonenum}{$desc}{amount} += $amount;
4132 $lines{$phonenum}{$desc}{calls}++;
4133 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4135 my $line = $usage_class{$detail->classnum}->classname;
4136 $sections{"$phonenum $line"} ||=
4140 'sort_weight' => $usage_class{$detail->classnum}->weight,
4141 'phonenum' => $phonenum,
4142 'header' => [ @header ],
4144 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4145 $sections{"$phonenum $line"}{calls}++;
4146 $sections{"$phonenum $line"}{duration} += $detail->duration;
4148 $lines{"$phonenum $line"}{$desc} ||= {
4149 description => &{$escape}($description),
4150 #pkgpart => $part_pkg->pkgpart,
4158 product_code => 'N/A',
4159 ext_description => [],
4162 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4163 $lines{"$phonenum $line"}{$desc}{calls}++;
4164 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4165 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4166 $detail->formatted('format' => $format);
4171 my %sectionmap = ();
4172 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4173 foreach ( keys %sections ) {
4174 my @header = @{ $sections{$_}{header} || [] };
4176 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4177 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4178 my $usage_class = $summary ? $simple : $usage_simple;
4179 my $ending = $summary ? ' usage charges' : '';
4182 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4184 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4185 'amount' => $sections{$_}{amount}, #subtotal
4186 'calls' => $sections{$_}{calls},
4187 'duration' => $sections{$_}{duration},
4189 'tax_section' => '',
4190 'phonenum' => $sections{$_}{phonenum},
4191 'sort_weight' => $sections{$_}{sort_weight},
4192 'post_total' => $summary, #inspire pagebreak
4194 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4195 qw( description_generator
4198 total_line_generator
4205 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4206 $a->{sort_weight} <=> $b->{sort_weight}
4211 foreach my $section ( keys %lines ) {
4212 foreach my $line ( keys %{$lines{$section}} ) {
4213 my $l = $lines{$section}{$line};
4214 $l->{section} = $sectionmap{$section};
4215 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4216 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4221 if($conf->exists('phone_usage_class_summary')) {
4222 # this only works with Latex
4226 # after this, we'll have only two sections per DID:
4227 # Calls Summary and Calls Detail
4228 foreach my $section ( @sections ) {
4229 if($section->{'post_total'}) {
4230 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4231 $section->{'total_line_generator'} = sub { '' };
4232 $section->{'total_generator'} = sub { '' };
4233 $section->{'header_generator'} = sub { '' };
4234 $section->{'description_generator'} = '';
4235 push @newsections, $section;
4236 my %calls_detail = %$section;
4237 $calls_detail{'post_total'} = '';
4238 $calls_detail{'sort_weight'} = '';
4239 $calls_detail{'description_generator'} = sub { '' };
4240 $calls_detail{'header_generator'} = sub {
4241 return ' & Date/Time & Called Number & Duration & Price'
4242 if $format eq 'latex';
4245 $calls_detail{'description'} = 'Calls Detail: '
4246 . $section->{'phonenum'};
4247 push @newsections, \%calls_detail;
4251 # after this, each usage class is collapsed/summarized into a single
4252 # line under the Calls Summary section
4253 foreach my $newsection ( @newsections ) {
4254 if($newsection->{'post_total'}) { # this means Calls Summary
4255 foreach my $section ( @sections ) {
4256 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4257 && !$section->{'post_total'});
4258 my $newdesc = $section->{'description'};
4259 my $tn = $section->{'phonenum'};
4260 $newdesc =~ s/$tn//g;
4261 my $line = { ext_description => [],
4265 calls => $section->{'calls'},
4266 section => $newsection,
4267 duration => $section->{'duration'},
4268 description => $newdesc,
4269 amount => sprintf("%.2f",$section->{'amount'}),
4270 product_code => 'N/A',
4272 push @newlines, $line;
4277 # after this, Calls Details is populated with all CDRs
4278 foreach my $newsection ( @newsections ) {
4279 if(!$newsection->{'post_total'}) { # this means Calls Details
4280 foreach my $line ( @lines ) {
4281 next unless (scalar(@{$line->{'ext_description'}}) &&
4282 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4284 my @extdesc = @{$line->{'ext_description'}};
4286 foreach my $extdesc ( @extdesc ) {
4287 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4288 push @newextdesc, $extdesc;
4290 $line->{'ext_description'} = \@newextdesc;
4291 $line->{'section'} = $newsection;
4292 push @newlines, $line;
4297 return(\@newsections, \@newlines);
4300 return(\@sections, \@lines);
4307 #my @display = scalar(@_)
4309 # : qw( _items_previous _items_pkg );
4310 # #: qw( _items_pkg );
4311 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4312 my @display = qw( _items_previous _items_pkg );
4315 foreach my $display ( @display ) {
4316 push @b, $self->$display(@_);
4321 sub _items_previous {
4323 my $cust_main = $self->cust_main;
4324 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4326 foreach ( @pr_cust_bill ) {
4327 my $date = $conf->exists('invoice_show_prior_due_date')
4328 ? 'due '. $_->due_date2str($date_format)
4329 : time2str($date_format, $_->_date);
4331 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4332 #'pkgpart' => 'N/A',
4334 'amount' => sprintf("%.2f", $_->owed),
4340 # 'description' => 'Previous Balance',
4341 # #'pkgpart' => 'N/A',
4342 # 'pkgnum' => 'N/A',
4343 # 'amount' => sprintf("%10.2f", $pr_total ),
4344 # 'ext_description' => [ map {
4345 # "Invoice ". $_->invnum.
4346 # " (". time2str("%x",$_->_date). ") ".
4347 # sprintf("%10.2f", $_->owed)
4348 # } @pr_cust_bill ],
4357 warn "$me _items_pkg searching for all package line items\n"
4360 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4362 warn "$me _items_pkg filtering line items\n"
4364 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4366 if ($options{section} && $options{section}->{condensed}) {
4368 warn "$me _items_pkg condensing section\n"
4372 local $Storable::canonical = 1;
4373 foreach ( @items ) {
4375 delete $item->{ref};
4376 delete $item->{ext_description};
4377 my $key = freeze($item);
4378 $itemshash{$key} ||= 0;
4379 $itemshash{$key} ++; # += $item->{quantity};
4381 @items = sort { $a->{description} cmp $b->{description} }
4382 map { my $i = thaw($_);
4383 $i->{quantity} = $itemshash{$_};
4385 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4391 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4398 return 0 unless $a->itemdesc cmp $b->itemdesc;
4399 return -1 if $b->itemdesc eq 'Tax';
4400 return 1 if $a->itemdesc eq 'Tax';
4401 return -1 if $b->itemdesc eq 'Other surcharges';
4402 return 1 if $a->itemdesc eq 'Other surcharges';
4403 $a->itemdesc cmp $b->itemdesc;
4408 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4409 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4412 sub _items_cust_bill_pkg {
4414 my $cust_bill_pkgs = shift;
4417 my $format = $opt{format} || '';
4418 my $escape_function = $opt{escape_function} || sub { shift };
4419 my $format_function = $opt{format_function} || '';
4420 my $unsquelched = $opt{unsquelched} || '';
4421 my $section = $opt{section}->{description} if $opt{section};
4422 my $summary_page = $opt{summary_page} || '';
4423 my $multilocation = $opt{multilocation} || '';
4424 my $multisection = $opt{multisection} || '';
4425 my $discount_show_always = 0;
4428 my ($s, $r, $u) = ( undef, undef, undef );
4429 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4432 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4435 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4436 && $conf->exists('discount-show-always'));
4438 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4439 if ( $_ && !$cust_bill_pkg->hidden ) {
4440 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4441 $_->{amount} =~ s/^\-0\.00$/0.00/;
4442 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4444 unless ( $_->{amount} == 0 && !$discount_show_always );
4449 foreach my $display ( grep { defined($section)
4450 ? $_->section eq $section
4453 #grep { !$_->summary || !$summary_page } # bunk!
4454 grep { !$_->summary || $multisection }
4455 $cust_bill_pkg->cust_bill_pkg_display
4459 warn "$me _items_cust_bill_pkg considering display item $display\n"
4462 my $type = $display->type;
4464 my $desc = $cust_bill_pkg->desc;
4465 $desc = substr($desc, 0, 50). '...'
4466 if $format eq 'latex' && length($desc) > 50;
4468 my %details_opt = ( 'format' => $format,
4469 'escape_function' => $escape_function,
4470 'format_function' => $format_function,
4473 if ( $cust_bill_pkg->pkgnum > 0 ) {
4475 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4478 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4480 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4482 warn "$me _items_cust_bill_pkg adding setup\n"
4485 my $description = $desc;
4486 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4489 unless ( $cust_pkg->part_pkg->hide_svc_detail
4490 || $cust_bill_pkg->hidden )
4493 push @d, map &{$escape_function}($_),
4494 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4495 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4497 if ( $multilocation ) {
4498 my $loc = $cust_pkg->location_label;
4499 $loc = substr($loc, 0, 50). '...'
4500 if $format eq 'latex' && length($loc) > 50;
4501 push @d, &{$escape_function}($loc);
4506 push @d, $cust_bill_pkg->details(%details_opt)
4507 if $cust_bill_pkg->recur == 0;
4509 if ( $cust_bill_pkg->hidden ) {
4510 $s->{amount} += $cust_bill_pkg->setup;
4511 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4512 push @{ $s->{ext_description} }, @d;
4515 description => $description,
4516 #pkgpart => $part_pkg->pkgpart,
4517 pkgnum => $cust_bill_pkg->pkgnum,
4518 amount => $cust_bill_pkg->setup,
4519 unit_amount => $cust_bill_pkg->unitsetup,
4520 quantity => $cust_bill_pkg->quantity,
4521 ext_description => \@d,
4527 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4528 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4529 ( !$type || $type eq 'R' || $type eq 'U' )
4533 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4536 my $is_summary = $display->summary;
4537 my $description = ($is_summary && $type && $type eq 'U')
4538 ? "Usage charges" : $desc;
4540 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4541 " - ". time2str($date_format, $cust_bill_pkg->edate).
4543 unless $conf->exists('disable_line_item_date_ranges');
4547 #at least until cust_bill_pkg has "past" ranges in addition to
4548 #the "future" sdate/edate ones... see #3032
4549 my @dates = ( $self->_date );
4550 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4551 push @dates, $prev->sdate if $prev;
4552 push @dates, undef if !$prev;
4554 unless ( $cust_pkg->part_pkg->hide_svc_detail
4555 || $cust_bill_pkg->itemdesc
4556 || $cust_bill_pkg->hidden
4557 || $is_summary && $type && $type eq 'U' )
4560 warn "$me _items_cust_bill_pkg adding service details\n"
4563 push @d, map &{$escape_function}($_),
4564 $cust_pkg->h_labels_short(@dates, 'I')
4565 #$cust_bill_pkg->edate,
4566 #$cust_bill_pkg->sdate)
4567 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4569 warn "$me _items_cust_bill_pkg done adding service details\n"
4572 if ( $multilocation ) {
4573 my $loc = $cust_pkg->location_label;
4574 $loc = substr($loc, 0, 50). '...'
4575 if $format eq 'latex' && length($loc) > 50;
4576 push @d, &{$escape_function}($loc);
4581 unless ( $is_summary ) {
4582 warn "$me _items_cust_bill_pkg adding details\n"
4585 #instead of omitting details entirely in this case (unwanted side
4586 # effects), just omit CDRs
4587 $details_opt{'format_function'} = sub { () }
4588 if $type && $type eq 'R';
4590 push @d, $cust_bill_pkg->details(%details_opt);
4593 warn "$me _items_cust_bill_pkg calculating amount\n"
4598 $amount = $cust_bill_pkg->recur;
4599 } elsif ($type eq 'R') {
4600 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4601 } elsif ($type eq 'U') {
4602 $amount = $cust_bill_pkg->usage;
4605 if ( !$type || $type eq 'R' ) {
4607 warn "$me _items_cust_bill_pkg adding recur\n"
4610 if ( $cust_bill_pkg->hidden ) {
4611 $r->{amount} += $amount;
4612 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4613 push @{ $r->{ext_description} }, @d;
4616 description => $description,
4617 #pkgpart => $part_pkg->pkgpart,
4618 pkgnum => $cust_bill_pkg->pkgnum,
4620 unit_amount => $cust_bill_pkg->unitrecur,
4621 quantity => $cust_bill_pkg->quantity,
4622 ext_description => \@d,
4626 } else { # $type eq 'U'
4628 warn "$me _items_cust_bill_pkg adding usage\n"
4631 if ( $cust_bill_pkg->hidden ) {
4632 $u->{amount} += $amount;
4633 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4634 push @{ $u->{ext_description} }, @d;
4637 description => $description,
4638 #pkgpart => $part_pkg->pkgpart,
4639 pkgnum => $cust_bill_pkg->pkgnum,
4641 unit_amount => $cust_bill_pkg->unitrecur,
4642 quantity => $cust_bill_pkg->quantity,
4643 ext_description => \@d,
4649 } # recurring or usage with recurring charge
4651 } else { #pkgnum tax or one-shot line item (??)
4653 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4656 if ( $cust_bill_pkg->setup != 0 ) {
4658 'description' => $desc,
4659 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4662 if ( $cust_bill_pkg->recur != 0 ) {
4664 'description' => "$desc (".
4665 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4666 time2str($date_format, $cust_bill_pkg->edate). ')',
4667 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4677 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4680 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4682 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4683 $_->{amount} =~ s/^\-0\.00$/0.00/;
4684 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4686 unless ( $_->{amount} == 0 && !$discount_show_always );
4694 sub _items_credits {
4695 my( $self, %opt ) = @_;
4696 my $trim_len = $opt{'trim_len'} || 60;
4700 foreach ( $self->cust_credited ) {
4702 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4704 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4705 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4706 $reason = " ($reason) " if $reason;
4709 #'description' => 'Credit ref\#'. $_->crednum.
4710 # " (". time2str("%x",$_->cust_credit->_date) .")".
4712 'description' => 'Credit applied '.
4713 time2str($date_format,$_->cust_credit->_date). $reason,
4714 'amount' => sprintf("%.2f",$_->amount),
4722 sub _items_payments {
4726 #get & print payments
4727 foreach ( $self->cust_bill_pay ) {
4729 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4732 'description' => "Payment received ".
4733 time2str($date_format,$_->cust_pay->_date ),
4734 'amount' => sprintf("%.2f", $_->amount )
4742 =item call_details [ OPTION => VALUE ... ]
4744 Returns an array of CSV strings representing the call details for this invoice
4745 The only option available is the boolean prepend_billed_number
4750 my ($self, %opt) = @_;
4752 my $format_function = sub { shift };
4754 if ($opt{prepend_billed_number}) {
4755 $format_function = sub {
4759 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4764 my @details = map { $_->details( 'format_function' => $format_function,
4765 'escape_function' => sub{ return() },
4769 $self->cust_bill_pkg;
4770 my $header = $details[0];
4771 ( $header, grep { $_ ne $header } @details );
4781 =item process_reprint
4785 sub process_reprint {
4786 process_re_X('print', @_);
4789 =item process_reemail
4793 sub process_reemail {
4794 process_re_X('email', @_);
4802 process_re_X('fax', @_);
4810 process_re_X('ftp', @_);
4817 sub process_respool {
4818 process_re_X('spool', @_);
4821 use Storable qw(thaw);
4825 my( $method, $job ) = ( shift, shift );
4826 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4828 my $param = thaw(decode_base64(shift));
4829 warn Dumper($param) if $DEBUG;
4840 my($method, $job, %param ) = @_;
4842 warn "re_X $method for job $job with param:\n".
4843 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4846 #some false laziness w/search/cust_bill.html
4848 my $orderby = 'ORDER BY cust_bill._date';
4850 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4852 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4854 my @cust_bill = qsearch( {
4855 #'select' => "cust_bill.*",
4856 'table' => 'cust_bill',
4857 'addl_from' => $addl_from,
4859 'extra_sql' => $extra_sql,
4860 'order_by' => $orderby,
4864 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4866 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4869 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4870 foreach my $cust_bill ( @cust_bill ) {
4871 $cust_bill->$method();
4873 if ( $job ) { #progressbar foo
4875 if ( time - $min_sec > $last ) {
4876 my $error = $job->update_statustext(
4877 int( 100 * $num / scalar(@cust_bill) )
4879 die $error if $error;
4890 =head1 CLASS METHODS
4896 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4901 my ($class, $start, $end) = @_;
4903 $class->paid_sql($start, $end). ' - '.
4904 $class->credited_sql($start, $end);
4909 Returns an SQL fragment to retreive the net amount (charged minus credited).
4914 my ($class, $start, $end) = @_;
4915 'charged - '. $class->credited_sql($start, $end);
4920 Returns an SQL fragment to retreive the amount paid against this invoice.
4925 my ($class, $start, $end) = @_;
4926 $start &&= "AND cust_bill_pay._date <= $start";
4927 $end &&= "AND cust_bill_pay._date > $end";
4928 $start = '' unless defined($start);
4929 $end = '' unless defined($end);
4930 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4931 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4936 Returns an SQL fragment to retreive the amount credited against this invoice.
4941 my ($class, $start, $end) = @_;
4942 $start &&= "AND cust_credit_bill._date <= $start";
4943 $end &&= "AND cust_credit_bill._date > $end";
4944 $start = '' unless defined($start);
4945 $end = '' unless defined($end);
4946 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4947 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4952 Returns an SQL fragment to retrieve the due date of an invoice.
4953 Currently only supported on PostgreSQL.
4961 cust_bill.invoice_terms,
4962 cust_main.invoice_terms,
4963 \''.($conf->config('invoice_default_terms') || '').'\'
4964 ), E\'Net (\\\\d+)\'
4966 ) * 86400 + cust_bill._date'
4969 =item search_sql_where HASHREF
4971 Class method which returns an SQL WHERE fragment to search for parameters
4972 specified in HASHREF. Valid parameters are
4978 List reference of start date, end date, as UNIX timestamps.
4988 List reference of charged limits (exclusive).
4992 List reference of charged limits (exclusive).
4996 flag, return open invoices only
5000 flag, return net invoices only
5004 =item newest_percust
5008 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5012 sub search_sql_where {
5013 my($class, $param) = @_;
5015 warn "$me search_sql_where called with params: \n".
5016 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5022 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5023 push @search, "cust_main.agentnum = $1";
5027 if ( $param->{_date} ) {
5028 my($beginning, $ending) = @{$param->{_date}};
5030 push @search, "cust_bill._date >= $beginning",
5031 "cust_bill._date < $ending";
5035 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5036 push @search, "cust_bill.invnum >= $1";
5038 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5039 push @search, "cust_bill.invnum <= $1";
5043 if ( $param->{charged} ) {
5044 my @charged = ref($param->{charged})
5045 ? @{ $param->{charged} }
5046 : ($param->{charged});
5048 push @search, map { s/^charged/cust_bill.charged/; $_; }
5052 my $owed_sql = FS::cust_bill->owed_sql;
5055 if ( $param->{owed} ) {
5056 my @owed = ref($param->{owed})
5057 ? @{ $param->{owed} }
5059 push @search, map { s/^owed/$owed_sql/; $_; }
5064 push @search, "0 != $owed_sql"
5065 if $param->{'open'};
5066 push @search, '0 != '. FS::cust_bill->net_sql
5070 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5071 if $param->{'days'};
5074 if ( $param->{'newest_percust'} ) {
5076 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5077 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5079 my @newest_where = map { my $x = $_;
5080 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5083 grep ! /^cust_main./, @search;
5084 my $newest_where = scalar(@newest_where)
5085 ? ' AND '. join(' AND ', @newest_where)
5089 push @search, "cust_bill._date = (
5090 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5091 WHERE newest_cust_bill.custnum = cust_bill.custnum
5097 #agent virtualization
5098 my $curuser = $FS::CurrentUser::CurrentUser;
5099 if ( $curuser->username eq 'fs_queue'
5100 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5102 my $newuser = qsearchs('access_user', {
5103 'username' => $username,
5107 $curuser = $newuser;
5109 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5112 push @search, $curuser->agentnums_sql;
5114 join(' AND ', @search );
5126 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5127 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base