4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max);
12 use Text::Template 1.20;
14 use String::ShellQuote;
17 use Storable qw( freeze thaw );
19 use FS::UID qw( datasrc );
20 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
21 use FS::Record qw( qsearch qsearchs dbh );
22 use FS::cust_main_Mixin;
24 use FS::cust_statement;
25 use FS::cust_bill_pkg;
26 use FS::cust_bill_pkg_display;
27 use FS::cust_bill_pkg_detail;
31 use FS::cust_credit_bill;
33 use FS::cust_pay_batch;
34 use FS::cust_bill_event;
37 use FS::cust_bill_pay;
38 use FS::cust_bill_pay_batch;
39 use FS::part_bill_event;
42 use FS::cust_bill_batch;
43 use FS::cust_bill_pay_pkg;
44 use FS::cust_credit_bill_pkg;
47 @ISA = qw( FS::cust_main_Mixin FS::Record );
50 $me = '[FS::cust_bill]';
52 #ask FS::UID to run this stuff for us later
53 FS::UID->install_callback( sub {
54 my $conf = new FS::Conf; #global
55 $money_char = $conf->config('money_char') || '$';
56 $date_format = $conf->config('date_format') || '%x'; #/YY
57 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
58 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
63 FS::cust_bill - Object methods for cust_bill records
69 $record = new FS::cust_bill \%hash;
70 $record = new FS::cust_bill { 'column' => 'value' };
72 $error = $record->insert;
74 $error = $new_record->replace($old_record);
76 $error = $record->delete;
78 $error = $record->check;
80 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
82 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
84 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
86 @cust_pay_objects = $cust_bill->cust_pay;
88 $tax_amount = $record->tax;
90 @lines = $cust_bill->print_text;
91 @lines = $cust_bill->print_text $time;
95 An FS::cust_bill object represents an invoice; a declaration that a customer
96 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
97 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
98 following fields are currently supported:
104 =item invnum - primary key (assigned automatically for new invoices)
106 =item custnum - customer (see L<FS::cust_main>)
108 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
109 L<Time::Local> and L<Date::Parse> for conversion functions.
111 =item charged - amount of this invoice
113 =item invoice_terms - optional terms override for this specific invoice
117 Customer info at invoice generation time
121 =item previous_balance
123 =item billing_balance
131 =item printed - deprecated
139 =item closed - books closed flag, empty or `Y'
141 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
143 =item agent_invid - legacy invoice number
153 Creates a new invoice. To add the invoice to the database, see L<"insert">.
154 Invoices are normally created by calling the bill method of a customer object
155 (see L<FS::cust_main>).
159 sub table { 'cust_bill'; }
161 sub cust_linked { $_[0]->cust_main_custnum; }
162 sub cust_unlinked_msg {
164 "WARNING: can't find cust_main.custnum ". $self->custnum.
165 ' (cust_bill.invnum '. $self->invnum. ')';
170 Adds this invoice to the database ("Posts" the invoice). If there is an error,
171 returns the error, otherwise returns false.
177 warn "$me insert called\n" if $DEBUG;
179 local $SIG{HUP} = 'IGNORE';
180 local $SIG{INT} = 'IGNORE';
181 local $SIG{QUIT} = 'IGNORE';
182 local $SIG{TERM} = 'IGNORE';
183 local $SIG{TSTP} = 'IGNORE';
184 local $SIG{PIPE} = 'IGNORE';
186 my $oldAutoCommit = $FS::UID::AutoCommit;
187 local $FS::UID::AutoCommit = 0;
190 my $error = $self->SUPER::insert;
192 $dbh->rollback if $oldAutoCommit;
196 if ( $self->get('cust_bill_pkg') ) {
197 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
198 $cust_bill_pkg->invnum($self->invnum);
199 my $error = $cust_bill_pkg->insert;
201 $dbh->rollback if $oldAutoCommit;
202 return "can't create invoice line item: $error";
207 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
214 This method now works but you probably shouldn't use it. Instead, apply a
215 credit against the invoice.
217 Using this method to delete invoices outright is really, really bad. There
218 would be no record you ever posted this invoice, and there are no check to
219 make sure charged = 0 or that there are no associated cust_bill_pkg records.
221 Really, don't use it.
227 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
229 local $SIG{HUP} = 'IGNORE';
230 local $SIG{INT} = 'IGNORE';
231 local $SIG{QUIT} = 'IGNORE';
232 local $SIG{TERM} = 'IGNORE';
233 local $SIG{TSTP} = 'IGNORE';
234 local $SIG{PIPE} = 'IGNORE';
236 my $oldAutoCommit = $FS::UID::AutoCommit;
237 local $FS::UID::AutoCommit = 0;
240 foreach my $table (qw(
253 foreach my $linked ( $self->$table() ) {
254 my $error = $linked->delete;
256 $dbh->rollback if $oldAutoCommit;
263 my $error = $self->SUPER::delete(@_);
265 $dbh->rollback if $oldAutoCommit;
269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275 =item replace [ OLD_RECORD ]
277 You can, but probably shouldn't modify invoices...
279 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
280 supplied, replaces this record. If there is an error, returns the error,
281 otherwise returns false.
285 #replace can be inherited from Record.pm
287 # replace_check is now the preferred way to #implement replace data checks
288 # (so $object->replace() works without an argument)
291 my( $new, $old ) = ( shift, shift );
292 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
293 #return "Can't change _date!" unless $old->_date eq $new->_date;
294 return "Can't change _date" unless $old->_date == $new->_date;
295 return "Can't change charged" unless $old->charged == $new->charged
296 || $old->charged == 0
297 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
303 =item add_cc_surcharge
309 sub add_cc_surcharge {
310 my ($self, $pkgnum, $amount) = (shift, shift, shift);
313 my $cust_bill_pkg = new FS::cust_bill_pkg({
314 'invnum' => $self->invnum,
318 $error = $cust_bill_pkg->insert;
319 return $error if $error;
321 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
322 $self->charged($self->charged+$amount);
323 $error = $self->replace;
324 return $error if $error;
326 $self->apply_payments_and_credits;
332 Checks all fields to make sure this is a valid invoice. If there is an error,
333 returns the error, otherwise returns false. Called by the insert and replace
342 $self->ut_numbern('invnum')
343 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
344 || $self->ut_numbern('_date')
345 || $self->ut_money('charged')
346 || $self->ut_numbern('printed')
347 || $self->ut_enum('closed', [ '', 'Y' ])
348 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
349 || $self->ut_numbern('agent_invid') #varchar?
351 return $error if $error;
353 $self->_date(time) unless $self->_date;
355 $self->printed(0) if $self->printed eq '';
362 Returns the displayed invoice number for this invoice: agent_invid if
363 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
369 my $conf = $self->conf;
370 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
371 return $self->agent_invid;
373 return $self->invnum;
379 Returns a list consisting of the total previous balance for this customer,
380 followed by the previous outstanding invoices (as FS::cust_bill objects also).
387 my @cust_bill = sort { $a->_date <=> $b->_date }
388 grep { $_->owed != 0 && $_->_date < $self->_date }
389 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
391 foreach ( @cust_bill ) { $total += $_->owed; }
397 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
404 { 'table' => 'cust_bill_pkg',
405 'hashref' => { 'invnum' => $self->invnum },
406 'order_by' => 'ORDER BY billpkgnum',
411 =item cust_bill_pkg_pkgnum PKGNUM
413 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
418 sub cust_bill_pkg_pkgnum {
419 my( $self, $pkgnum ) = @_;
421 { 'table' => 'cust_bill_pkg',
422 'hashref' => { 'invnum' => $self->invnum,
425 'order_by' => 'ORDER BY billpkgnum',
432 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
439 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
440 $self->cust_bill_pkg;
442 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
447 Returns true if any of the packages (or their definitions) corresponding to the
448 line items for this invoice have the no_auto flag set.
454 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
457 =item open_cust_bill_pkg
459 Returns the open line items for this invoice.
461 Note that cust_bill_pkg with both setup and recur fees are returned as two
462 separate line items, each with only one fee.
466 # modeled after cust_main::open_cust_bill
467 sub open_cust_bill_pkg {
470 # grep { $_->owed > 0 } $self->cust_bill_pkg
472 my %other = ( 'recur' => 'setup',
473 'setup' => 'recur', );
475 foreach my $field ( qw( recur setup )) {
476 push @open, map { $_->set( $other{$field}, 0 ); $_; }
477 grep { $_->owed($field) > 0 }
478 $self->cust_bill_pkg;
484 =item cust_bill_event
486 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
490 sub cust_bill_event {
492 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
495 =item num_cust_bill_event
497 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
501 sub num_cust_bill_event {
504 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
505 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
506 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
507 $sth->fetchrow_arrayref->[0];
512 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
516 #false laziness w/cust_pkg.pm
520 'table' => 'cust_event',
521 'addl_from' => 'JOIN part_event USING ( eventpart )',
522 'hashref' => { 'tablenum' => $self->invnum },
523 'extra_sql' => " AND eventtable = 'cust_bill' ",
529 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
533 #false laziness w/cust_pkg.pm
537 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
538 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
539 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
540 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
541 $sth->fetchrow_arrayref->[0];
546 Returns the customer (see L<FS::cust_main>) for this invoice.
552 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
555 =item cust_suspend_if_balance_over AMOUNT
557 Suspends the customer associated with this invoice if the total amount owed on
558 this invoice and all older invoices is greater than the specified amount.
560 Returns a list: an empty list on success or a list of errors.
564 sub cust_suspend_if_balance_over {
565 my( $self, $amount ) = ( shift, shift );
566 my $cust_main = $self->cust_main;
567 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
570 $cust_main->suspend(@_);
576 Depreciated. See the cust_credited method.
578 #Returns a list consisting of the total previous credited (see
579 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
580 #outstanding credits (FS::cust_credit objects).
586 croak "FS::cust_bill->cust_credit depreciated; see ".
587 "FS::cust_bill->cust_credit_bill";
590 #my @cust_credit = sort { $a->_date <=> $b->_date }
591 # grep { $_->credited != 0 && $_->_date < $self->_date }
592 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
594 #foreach (@cust_credit) { $total += $_->credited; }
595 #$total, @cust_credit;
600 Depreciated. See the cust_bill_pay method.
602 #Returns all payments (see L<FS::cust_pay>) for this invoice.
608 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
610 #sort { $a->_date <=> $b->_date }
611 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
617 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
620 sub cust_bill_pay_batch {
622 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
627 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
633 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
634 sort { $a->_date <=> $b->_date }
635 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
640 =item cust_credit_bill
642 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
648 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
649 sort { $a->_date <=> $b->_date }
650 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
654 sub cust_credit_bill {
655 shift->cust_credited(@_);
658 #=item cust_bill_pay_pkgnum PKGNUM
660 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
661 #with matching pkgnum.
665 #sub cust_bill_pay_pkgnum {
666 # my( $self, $pkgnum ) = @_;
667 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
668 # sort { $a->_date <=> $b->_date }
669 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
670 # 'pkgnum' => $pkgnum,
675 =item cust_bill_pay_pkg PKGNUM
677 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
678 applied against the matching pkgnum.
682 sub cust_bill_pay_pkg {
683 my( $self, $pkgnum ) = @_;
686 'select' => 'cust_bill_pay_pkg.*',
687 'table' => 'cust_bill_pay_pkg',
688 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
689 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
690 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
691 " AND cust_bill_pkg.pkgnum = $pkgnum",
696 #=item cust_credited_pkgnum PKGNUM
698 #=item cust_credit_bill_pkgnum PKGNUM
700 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
701 #with matching pkgnum.
705 #sub cust_credited_pkgnum {
706 # my( $self, $pkgnum ) = @_;
707 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
708 # sort { $a->_date <=> $b->_date }
709 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
710 # 'pkgnum' => $pkgnum,
715 #sub cust_credit_bill_pkgnum {
716 # shift->cust_credited_pkgnum(@_);
719 =item cust_credit_bill_pkg PKGNUM
721 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
722 applied against the matching pkgnum.
726 sub cust_credit_bill_pkg {
727 my( $self, $pkgnum ) = @_;
730 'select' => 'cust_credit_bill_pkg.*',
731 'table' => 'cust_credit_bill_pkg',
732 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
733 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
734 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
735 " AND cust_bill_pkg.pkgnum = $pkgnum",
740 =item cust_bill_batch
742 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
746 sub cust_bill_batch {
748 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
753 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
760 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
762 foreach (@taxlines) { $total += $_->setup; }
768 Returns the amount owed (still outstanding) on this invoice, which is charged
769 minus all payment applications (see L<FS::cust_bill_pay>) and credit
770 applications (see L<FS::cust_credit_bill>).
776 my $balance = $self->charged;
777 $balance -= $_->amount foreach ( $self->cust_bill_pay );
778 $balance -= $_->amount foreach ( $self->cust_credited );
779 $balance = sprintf( "%.2f", $balance);
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
785 my( $self, $pkgnum ) = @_;
787 #my $balance = $self->charged;
789 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
791 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
792 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
794 $balance = sprintf( "%.2f", $balance);
795 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
799 =item apply_payments_and_credits [ OPTION => VALUE ... ]
801 Applies unapplied payments and credits to this invoice.
803 A hash of optional arguments may be passed. Currently "manual" is supported.
804 If true, a payment receipt is sent instead of a statement when
805 'payment_receipt_email' configuration option is set.
807 If there is an error, returns the error, otherwise returns false.
811 sub apply_payments_and_credits {
812 my( $self, %options ) = @_;
813 my $conf = $self->conf;
815 local $SIG{HUP} = 'IGNORE';
816 local $SIG{INT} = 'IGNORE';
817 local $SIG{QUIT} = 'IGNORE';
818 local $SIG{TERM} = 'IGNORE';
819 local $SIG{TSTP} = 'IGNORE';
820 local $SIG{PIPE} = 'IGNORE';
822 my $oldAutoCommit = $FS::UID::AutoCommit;
823 local $FS::UID::AutoCommit = 0;
826 $self->select_for_update; #mutex
828 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
829 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
831 if ( $conf->exists('pkg-balances') ) {
832 # limit @payments & @credits to those w/ a pkgnum grepped from $self
833 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
834 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
835 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
838 while ( $self->owed > 0 and ( @payments || @credits ) ) {
841 if ( @payments && @credits ) {
843 #decide which goes first by weight of top (unapplied) line item
845 my @open_lineitems = $self->open_cust_bill_pkg;
848 max( map { $_->part_pkg->pay_weight || 0 }
853 my $max_credit_weight =
854 max( map { $_->part_pkg->credit_weight || 0 }
860 #if both are the same... payments first? it has to be something
861 if ( $max_pay_weight >= $max_credit_weight ) {
867 } elsif ( @payments ) {
869 } elsif ( @credits ) {
872 die "guru meditation #12 and 35";
876 if ( $app eq 'pay' ) {
878 my $payment = shift @payments;
879 $unapp_amount = $payment->unapplied;
880 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
881 $app->pkgnum( $payment->pkgnum )
882 if $conf->exists('pkg-balances') && $payment->pkgnum;
884 } elsif ( $app eq 'credit' ) {
886 my $credit = shift @credits;
887 $unapp_amount = $credit->credited;
888 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
889 $app->pkgnum( $credit->pkgnum )
890 if $conf->exists('pkg-balances') && $credit->pkgnum;
893 die "guru meditation #12 and 35";
897 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
898 warn "owed_pkgnum ". $app->pkgnum;
899 $owed = $self->owed_pkgnum($app->pkgnum);
903 next unless $owed > 0;
905 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
906 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
908 $app->invnum( $self->invnum );
910 my $error = $app->insert(%options);
912 $dbh->rollback if $oldAutoCommit;
913 return "Error inserting ". $app->table. " record: $error";
915 die $error if $error;
919 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
924 =item generate_email OPTION => VALUE ...
932 sender address, required
936 alternate template name, optional
940 text attachment arrayref, optional
944 email subject, optional
948 notice name instead of "Invoice", optional
952 Returns an argument list to be passed to L<FS::Misc::send_email>.
962 my $conf = $self->conf;
964 my $me = '[FS::cust_bill::generate_email]';
967 'from' => $args{'from'},
968 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
972 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
973 'template' => $args{'template'},
974 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
975 'no_coupon' => $args{'no_coupon'},
978 my $cust_main = $self->cust_main;
980 if (ref($args{'to'}) eq 'ARRAY') {
981 $return{'to'} = $args{'to'};
983 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
984 $cust_main->invoicing_list
988 if ( $conf->exists('invoice_html') ) {
990 warn "$me creating HTML/text multipart message"
993 $return{'nobody'} = 1;
995 my $alternative = build MIME::Entity
996 'Type' => 'multipart/alternative',
997 #'Encoding' => '7bit',
998 'Disposition' => 'inline'
1002 if ( $conf->exists('invoice_email_pdf')
1003 and scalar($conf->config('invoice_email_pdf_note')) ) {
1005 warn "$me using 'invoice_email_pdf_note' in multipart message"
1007 $data = [ map { $_ . "\n" }
1008 $conf->config('invoice_email_pdf_note')
1013 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1015 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1016 $data = $args{'print_text'};
1018 $data = [ $self->print_text(\%opt) ];
1023 $alternative->attach(
1024 'Type' => 'text/plain',
1025 'Encoding' => 'quoted-printable',
1026 #'Encoding' => '7bit',
1028 'Disposition' => 'inline',
1031 $args{'from'} =~ /\@([\w\.\-]+)/;
1032 my $from = $1 || 'example.com';
1033 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1036 my $agentnum = $cust_main->agentnum;
1037 if ( defined($args{'template'}) && length($args{'template'})
1038 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1041 $logo = 'logo_'. $args{'template'}. '.png';
1045 my $image_data = $conf->config_binary( $logo, $agentnum);
1047 my $image = build MIME::Entity
1048 'Type' => 'image/png',
1049 'Encoding' => 'base64',
1050 'Data' => $image_data,
1051 'Filename' => 'logo.png',
1052 'Content-ID' => "<$content_id>",
1056 if($conf->exists('invoice-barcode')){
1057 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1058 $barcode = build MIME::Entity
1059 'Type' => 'image/png',
1060 'Encoding' => 'base64',
1061 'Data' => $self->invoice_barcode(0),
1062 'Filename' => 'barcode.png',
1063 'Content-ID' => "<$barcode_content_id>",
1065 $opt{'barcode_cid'} = $barcode_content_id;
1068 $alternative->attach(
1069 'Type' => 'text/html',
1070 'Encoding' => 'quoted-printable',
1071 'Data' => [ '<html>',
1074 ' '. encode_entities($return{'subject'}),
1077 ' <body bgcolor="#e8e8e8">',
1078 $self->print_html({ 'cid'=>$content_id, %opt }),
1082 'Disposition' => 'inline',
1083 #'Filename' => 'invoice.pdf',
1086 my @otherparts = ();
1087 if ( $cust_main->email_csv_cdr ) {
1089 push @otherparts, build MIME::Entity
1090 'Type' => 'text/csv',
1091 'Encoding' => '7bit',
1092 'Data' => [ map { "$_\n" }
1093 $self->call_details('prepend_billed_number' => 1)
1095 'Disposition' => 'attachment',
1096 'Filename' => 'usage-'. $self->invnum. '.csv',
1101 if ( $conf->exists('invoice_email_pdf') ) {
1106 # multipart/alternative
1112 my $related = build MIME::Entity 'Type' => 'multipart/related',
1113 'Encoding' => '7bit';
1115 #false laziness w/Misc::send_email
1116 $related->head->replace('Content-type',
1117 $related->mime_type.
1118 '; boundary="'. $related->head->multipart_boundary. '"'.
1119 '; type=multipart/alternative'
1122 $related->add_part($alternative);
1124 $related->add_part($image);
1126 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1128 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1132 #no other attachment:
1134 # multipart/alternative
1139 $return{'content-type'} = 'multipart/related';
1140 if($conf->exists('invoice-barcode')){
1141 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1144 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1146 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1147 #$return{'disposition'} = 'inline';
1153 if ( $conf->exists('invoice_email_pdf') ) {
1154 warn "$me creating PDF attachment"
1157 #mime parts arguments a la MIME::Entity->build().
1158 $return{'mimeparts'} = [
1159 { $self->mimebuild_pdf(\%opt) }
1163 if ( $conf->exists('invoice_email_pdf')
1164 and scalar($conf->config('invoice_email_pdf_note')) ) {
1166 warn "$me using 'invoice_email_pdf_note'"
1168 $return{'body'} = [ map { $_ . "\n" }
1169 $conf->config('invoice_email_pdf_note')
1174 warn "$me not using 'invoice_email_pdf_note'"
1176 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1177 $return{'body'} = $args{'print_text'};
1179 $return{'body'} = [ $self->print_text(\%opt) ];
1192 Returns a list suitable for passing to MIME::Entity->build(), representing
1193 this invoice as PDF attachment.
1200 'Type' => 'application/pdf',
1201 'Encoding' => 'base64',
1202 'Data' => [ $self->print_pdf(@_) ],
1203 'Disposition' => 'attachment',
1204 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1208 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1210 Sends this invoice to the destinations configured for this customer: sends
1211 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1213 Options can be passed as a hashref (recommended) or as a list of up to
1214 four values for templatename, agentnum, invoice_from and amount.
1216 I<template>, if specified, is the name of a suffix for alternate invoices.
1218 I<agentnum>, if specified, means that this invoice will only be sent for customers
1219 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1220 single agent) or an arrayref of agentnums.
1222 I<invoice_from>, if specified, overrides the default email invoice From: address.
1224 I<amount>, if specified, only sends the invoice if the total amount owed on this
1225 invoice and all older invoices is greater than the specified amount.
1227 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1231 sub queueable_send {
1234 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1235 or die "invalid invoice number: " . $opt{invnum};
1237 my @args = ( $opt{template}, $opt{agentnum} );
1238 push @args, $opt{invoice_from}
1239 if exists($opt{invoice_from}) && $opt{invoice_from};
1241 my $error = $self->send( @args );
1242 die $error if $error;
1248 my $conf = $self->conf;
1250 my( $template, $invoice_from, $notice_name );
1252 my $balance_over = 0;
1256 $template = $opt->{'template'} || '';
1257 if ( $agentnums = $opt->{'agentnum'} ) {
1258 $agentnums = [ $agentnums ] unless ref($agentnums);
1260 $invoice_from = $opt->{'invoice_from'};
1261 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1262 $notice_name = $opt->{'notice_name'};
1264 $template = scalar(@_) ? shift : '';
1265 if ( scalar(@_) && $_[0] ) {
1266 $agentnums = ref($_[0]) ? shift : [ shift ];
1268 $invoice_from = shift if scalar(@_);
1269 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1272 return 'N/A' unless ! $agentnums
1273 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1276 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1278 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1279 $conf->config('invoice_from', $self->cust_main->agentnum );
1282 'template' => $template,
1283 'invoice_from' => $invoice_from,
1284 'notice_name' => ( $notice_name || 'Invoice' ),
1287 my @invoicing_list = $self->cust_main->invoicing_list;
1289 #$self->email_invoice(\%opt)
1291 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1293 #$self->print_invoice(\%opt)
1295 if grep { $_ eq 'POST' } @invoicing_list; #postal
1297 $self->fax_invoice(\%opt)
1298 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1304 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1306 Emails this invoice.
1308 Options can be passed as a hashref (recommended) or as a list of up to
1309 two values for templatename and invoice_from.
1311 I<template>, if specified, is the name of a suffix for alternate invoices.
1313 I<invoice_from>, if specified, overrides the default email invoice From: address.
1315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1319 sub queueable_email {
1322 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1323 or die "invalid invoice number: " . $opt{invnum};
1325 my %args = ( 'template' => $opt{template} );
1326 $args{$_} = $opt{$_}
1327 foreach grep { exists($opt{$_}) && $opt{$_} }
1328 qw( invoice_from notice_name no_coupon );
1330 my $error = $self->email( \%args );
1331 die $error if $error;
1335 #sub email_invoice {
1338 my $conf = $self->conf;
1340 my( $template, $invoice_from, $notice_name, $no_coupon );
1343 $template = $opt->{'template'} || '';
1344 $invoice_from = $opt->{'invoice_from'};
1345 $notice_name = $opt->{'notice_name'} || 'Invoice';
1346 $no_coupon = $opt->{'no_coupon'} || 0;
1348 $template = scalar(@_) ? shift : '';
1349 $invoice_from = shift if scalar(@_);
1350 $notice_name = 'Invoice';
1354 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1355 $conf->config('invoice_from', $self->cust_main->agentnum );
1357 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1358 $self->cust_main->invoicing_list;
1360 if ( ! @invoicing_list ) { #no recipients
1361 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1362 die 'No recipients for customer #'. $self->custnum;
1364 #default: better to notify this person than silence
1365 @invoicing_list = ($invoice_from);
1369 my $subject = $self->email_subject($template);
1371 my $error = send_email(
1372 $self->generate_email(
1373 'from' => $invoice_from,
1374 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1375 'subject' => $subject,
1376 'template' => $template,
1377 'notice_name' => $notice_name,
1378 'no_coupon' => $no_coupon,
1381 die "can't email invoice: $error\n" if $error;
1382 #die "$error\n" if $error;
1388 my $conf = $self->conf;
1390 #my $template = scalar(@_) ? shift : '';
1393 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1396 my $cust_main = $self->cust_main;
1397 my $name = $cust_main->name;
1398 my $name_short = $cust_main->name_short;
1399 my $invoice_number = $self->invnum;
1400 my $invoice_date = $self->_date_pretty;
1402 eval qq("$subject");
1405 =item lpr_data HASHREF | [ TEMPLATE ]
1407 Returns the postscript or plaintext for this invoice as an arrayref.
1409 Options can be passed as a hashref (recommended) or as a single optional value
1412 I<template>, if specified, is the name of a suffix for alternate invoices.
1414 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1420 my $conf = $self->conf;
1421 my( $template, $notice_name );
1424 $template = $opt->{'template'} || '';
1425 $notice_name = $opt->{'notice_name'} || 'Invoice';
1427 $template = scalar(@_) ? shift : '';
1428 $notice_name = 'Invoice';
1432 'template' => $template,
1433 'notice_name' => $notice_name,
1436 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1437 [ $self->$method( \%opt ) ];
1440 =item print HASHREF | [ TEMPLATE ]
1442 Prints this invoice.
1444 Options can be passed as a hashref (recommended) or as a single optional
1447 I<template>, if specified, is the name of a suffix for alternate invoices.
1449 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1453 #sub print_invoice {
1456 my $conf = $self->conf;
1457 my( $template, $notice_name );
1460 $template = $opt->{'template'} || '';
1461 $notice_name = $opt->{'notice_name'} || 'Invoice';
1463 $template = scalar(@_) ? shift : '';
1464 $notice_name = 'Invoice';
1468 'template' => $template,
1469 'notice_name' => $notice_name,
1472 if($conf->exists('invoice_print_pdf')) {
1473 # Add the invoice to the current batch.
1474 $self->batch_invoice(\%opt);
1477 do_print $self->lpr_data(\%opt);
1481 =item fax_invoice HASHREF | [ TEMPLATE ]
1485 Options can be passed as a hashref (recommended) or as a single optional
1488 I<template>, if specified, is the name of a suffix for alternate invoices.
1490 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1496 my $conf = $self->conf;
1497 my( $template, $notice_name );
1500 $template = $opt->{'template'} || '';
1501 $notice_name = $opt->{'notice_name'} || 'Invoice';
1503 $template = scalar(@_) ? shift : '';
1504 $notice_name = 'Invoice';
1507 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1508 unless $conf->exists('invoice_latex');
1510 my $dialstring = $self->cust_main->getfield('fax');
1514 'template' => $template,
1515 'notice_name' => $notice_name,
1518 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1519 'dialstring' => $dialstring,
1521 die $error if $error;
1525 =item batch_invoice [ HASHREF ]
1527 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1528 isn't an open batch, one will be created.
1533 my ($self, $opt) = @_;
1534 my $bill_batch = $self->get_open_bill_batch;
1535 my $cust_bill_batch = FS::cust_bill_batch->new({
1536 batchnum => $bill_batch->batchnum,
1537 invnum => $self->invnum,
1539 return $cust_bill_batch->insert($opt);
1542 =item get_open_batch
1544 Returns the currently open batch as an FS::bill_batch object, creating a new
1545 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1550 sub get_open_bill_batch {
1552 my $conf = $self->conf;
1553 my $hashref = { status => 'O' };
1554 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1555 ? $self->cust_main->agentnum
1557 my $batch = qsearchs('bill_batch', $hashref);
1558 return $batch if $batch;
1559 $batch = FS::bill_batch->new($hashref);
1560 my $error = $batch->insert;
1561 die $error if $error;
1565 =item ftp_invoice [ TEMPLATENAME ]
1567 Sends this invoice data via FTP.
1569 TEMPLATENAME is unused?
1575 my $conf = $self->conf;
1576 my $template = scalar(@_) ? shift : '';
1579 'protocol' => 'ftp',
1580 'server' => $conf->config('cust_bill-ftpserver'),
1581 'username' => $conf->config('cust_bill-ftpusername'),
1582 'password' => $conf->config('cust_bill-ftppassword'),
1583 'dir' => $conf->config('cust_bill-ftpdir'),
1584 'format' => $conf->config('cust_bill-ftpformat'),
1588 =item spool_invoice [ TEMPLATENAME ]
1590 Spools this invoice data (see L<FS::spool_csv>)
1592 TEMPLATENAME is unused?
1598 my $conf = $self->conf;
1599 my $template = scalar(@_) ? shift : '';
1602 'format' => $conf->config('cust_bill-spoolformat'),
1603 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1607 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1609 Like B<send>, but only sends the invoice if it is the newest open invoice for
1614 sub send_if_newest {
1619 grep { $_->owed > 0 }
1620 qsearch('cust_bill', {
1621 'custnum' => $self->custnum,
1622 #'_date' => { op=>'>', value=>$self->_date },
1623 'invnum' => { op=>'>', value=>$self->invnum },
1630 =item send_csv OPTION => VALUE, ...
1632 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1636 protocol - currently only "ftp"
1642 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1643 and YYMMDDHHMMSS is a timestamp.
1645 See L</print_csv> for a description of the output format.
1650 my($self, %opt) = @_;
1654 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1655 mkdir $spooldir, 0700 unless -d $spooldir;
1657 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1658 my $file = "$spooldir/$tracctnum.csv";
1660 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1662 open(CSV, ">$file") or die "can't open $file: $!";
1670 if ( $opt{protocol} eq 'ftp' ) {
1671 eval "use Net::FTP;";
1673 $net = Net::FTP->new($opt{server}) or die @$;
1675 die "unknown protocol: $opt{protocol}";
1678 $net->login( $opt{username}, $opt{password} )
1679 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1681 $net->binary or die "can't set binary mode";
1683 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1685 $net->put($file) or die "can't put $file: $!";
1695 Spools CSV invoice data.
1701 =item format - 'default' or 'billco'
1703 =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>).
1705 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1707 =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.
1714 my($self, %opt) = @_;
1716 my $cust_main = $self->cust_main;
1718 if ( $opt{'dest'} ) {
1719 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1720 $cust_main->invoicing_list;
1721 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1722 || ! keys %invoicing_list;
1725 if ( $opt{'balanceover'} ) {
1727 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1730 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1731 mkdir $spooldir, 0700 unless -d $spooldir;
1733 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1737 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1738 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1741 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1743 open(CSV, ">>$file") or die "can't open $file: $!";
1744 flock(CSV, LOCK_EX);
1749 if ( lc($opt{'format'}) eq 'billco' ) {
1751 flock(CSV, LOCK_UN);
1756 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1759 open(CSV,">>$file") or die "can't open $file: $!";
1760 flock(CSV, LOCK_EX);
1766 flock(CSV, LOCK_UN);
1773 =item print_csv OPTION => VALUE, ...
1775 Returns CSV data for this invoice.
1779 format - 'default' or 'billco'
1781 Returns a list consisting of two scalars. The first is a single line of CSV
1782 header information for this invoice. The second is one or more lines of CSV
1783 detail information for this invoice.
1785 If I<format> is not specified or "default", the fields of the CSV file are as
1788 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1792 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1794 B<record_type> is C<cust_bill> for the initial header line only. The
1795 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1796 fields are filled in.
1798 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1799 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1802 =item invnum - invoice number
1804 =item custnum - customer number
1806 =item _date - invoice date
1808 =item charged - total invoice amount
1810 =item first - customer first name
1812 =item last - customer first name
1814 =item company - company name
1816 =item address1 - address line 1
1818 =item address2 - address line 1
1828 =item pkg - line item description
1830 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1832 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1834 =item sdate - start date for recurring fee
1836 =item edate - end date for recurring fee
1840 If I<format> is "billco", the fields of the header CSV file are as follows:
1842 +-------------------------------------------------------------------+
1843 | FORMAT HEADER FILE |
1844 |-------------------------------------------------------------------|
1845 | Field | Description | Name | Type | Width |
1846 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1847 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1848 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1849 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1850 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1851 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1852 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1853 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1854 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1855 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1856 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1857 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1858 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1859 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1860 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1861 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1862 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1863 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1864 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1865 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1866 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1867 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1868 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1869 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1870 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1871 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1872 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1873 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1874 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1875 +-------+-------------------------------+------------+------+-------+
1877 If I<format> is "billco", the fields of the detail CSV file are as follows:
1879 FORMAT FOR DETAIL FILE
1881 Field | Description | Name | Type | Width
1882 1 | N/A-Leave Empty | RC | CHAR | 2
1883 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1884 3 | Account Number | TRACCTNUM | CHAR | 15
1885 4 | Invoice Number | TRINVOICE | CHAR | 15
1886 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1887 6 | Transaction Detail | DETAILS | CHAR | 100
1888 7 | Amount | AMT | NUM* | 9
1889 8 | Line Format Control** | LNCTRL | CHAR | 2
1890 9 | Grouping Code | GROUP | CHAR | 2
1891 10 | User Defined | ACCT CODE | CHAR | 15
1896 my($self, %opt) = @_;
1898 eval "use Text::CSV_XS";
1901 my $cust_main = $self->cust_main;
1903 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1905 if ( lc($opt{'format'}) eq 'billco' ) {
1908 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1910 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1912 my( $previous_balance, @unused ) = $self->previous; #previous balance
1914 my $pmt_cr_applied = 0;
1915 $pmt_cr_applied += $_->{'amount'}
1916 foreach ( $self->_items_payments, $self->_items_credits ) ;
1918 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1921 '', # 1 | N/A-Leave Empty CHAR 2
1922 '', # 2 | N/A-Leave Empty CHAR 15
1923 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1924 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1925 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1926 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1927 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1928 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1929 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1930 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1931 '', # 10 | Ancillary Billing Information CHAR 30
1932 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1933 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1936 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1939 $duedate, # 14 | Bill Due Date CHAR 10
1941 $previous_balance, # 15 | Previous Balance NUM* 9
1942 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1943 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1944 $totaldue, # 18 | Total Amt Due NUM* 9
1945 $totaldue, # 19 | Total Amt Due NUM* 9
1946 '', # 20 | 30 Day Aging NUM* 9
1947 '', # 21 | 60 Day Aging NUM* 9
1948 '', # 22 | 90 Day Aging NUM* 9
1949 'N', # 23 | Y/N CHAR 1
1950 '', # 24 | Remittance automation CHAR 100
1951 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1952 $self->custnum, # 26 | Customer Reference Number CHAR 15
1953 '0', # 27 | Federal Tax*** NUM* 9
1954 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1955 '0', # 29 | Other Taxes & Fees*** NUM* 9
1964 time2str("%x", $self->_date),
1965 sprintf("%.2f", $self->charged),
1966 ( map { $cust_main->getfield($_) }
1967 qw( first last company address1 address2 city state zip country ) ),
1969 ) or die "can't create csv";
1972 my $header = $csv->string. "\n";
1975 if ( lc($opt{'format'}) eq 'billco' ) {
1978 foreach my $item ( $self->_items_pkg ) {
1981 '', # 1 | N/A-Leave Empty CHAR 2
1982 '', # 2 | N/A-Leave Empty CHAR 15
1983 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1984 $self->invnum, # 4 | Invoice Number CHAR 15
1985 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1986 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1987 $item->{'amount'}, # 7 | Amount NUM* 9
1988 '', # 8 | Line Format Control** CHAR 2
1989 '', # 9 | Grouping Code CHAR 2
1990 '', # 10 | User Defined CHAR 15
1993 $detail .= $csv->string. "\n";
1999 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2001 my($pkg, $setup, $recur, $sdate, $edate);
2002 if ( $cust_bill_pkg->pkgnum ) {
2004 ($pkg, $setup, $recur, $sdate, $edate) = (
2005 $cust_bill_pkg->part_pkg->pkg,
2006 ( $cust_bill_pkg->setup != 0
2007 ? sprintf("%.2f", $cust_bill_pkg->setup )
2009 ( $cust_bill_pkg->recur != 0
2010 ? sprintf("%.2f", $cust_bill_pkg->recur )
2012 ( $cust_bill_pkg->sdate
2013 ? time2str("%x", $cust_bill_pkg->sdate)
2015 ($cust_bill_pkg->edate
2016 ?time2str("%x", $cust_bill_pkg->edate)
2020 } else { #pkgnum tax
2021 next unless $cust_bill_pkg->setup != 0;
2022 $pkg = $cust_bill_pkg->desc;
2023 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2024 ( $sdate, $edate ) = ( '', '' );
2030 ( map { '' } (1..11) ),
2031 ($pkg, $setup, $recur, $sdate, $edate)
2032 ) or die "can't create csv";
2034 $detail .= $csv->string. "\n";
2040 ( $header, $detail );
2046 Pays this invoice with a compliemntary payment. If there is an error,
2047 returns the error, otherwise returns false.
2053 my $cust_pay = new FS::cust_pay ( {
2054 'invnum' => $self->invnum,
2055 'paid' => $self->owed,
2058 'payinfo' => $self->cust_main->payinfo,
2066 Attempts to pay this invoice with a credit card payment via a
2067 Business::OnlinePayment realtime gateway. See
2068 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2069 for supported processors.
2075 $self->realtime_bop( 'CC', @_ );
2080 Attempts to pay this invoice with an electronic check (ACH) payment via a
2081 Business::OnlinePayment realtime gateway. See
2082 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2083 for supported processors.
2089 $self->realtime_bop( 'ECHECK', @_ );
2094 Attempts to pay this invoice with phone bill (LEC) payment via a
2095 Business::OnlinePayment realtime gateway. See
2096 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2097 for supported processors.
2103 $self->realtime_bop( 'LEC', @_ );
2107 my( $self, $method ) = (shift,shift);
2108 my $conf = $self->conf;
2111 my $cust_main = $self->cust_main;
2112 my $balance = $cust_main->balance;
2113 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2114 $amount = sprintf("%.2f", $amount);
2115 return "not run (balance $balance)" unless $amount > 0;
2117 my $description = 'Internet Services';
2118 if ( $conf->exists('business-onlinepayment-description') ) {
2119 my $dtempl = $conf->config('business-onlinepayment-description');
2121 my $agent_obj = $cust_main->agent
2122 or die "can't retreive agent for $cust_main (agentnum ".
2123 $cust_main->agentnum. ")";
2124 my $agent = $agent_obj->agent;
2125 my $pkgs = join(', ',
2126 map { $_->part_pkg->pkg }
2127 grep { $_->pkgnum } $self->cust_bill_pkg
2129 $description = eval qq("$dtempl");
2132 $cust_main->realtime_bop($method, $amount,
2133 'description' => $description,
2134 'invnum' => $self->invnum,
2135 #this didn't do what we want, it just calls apply_payments_and_credits
2137 'apply_to_invoice' => 1,
2140 #this changes application behavior: auto payments
2141 #triggered against a specific invoice are now applied
2142 #to that invoice instead of oldest open.
2148 =item batch_card OPTION => VALUE...
2150 Adds a payment for this invoice to the pending credit card batch (see
2151 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2152 runs the payment using a realtime gateway.
2157 my ($self, %options) = @_;
2158 my $cust_main = $self->cust_main;
2160 $options{invnum} = $self->invnum;
2162 $cust_main->batch_card(%options);
2165 sub _agent_template {
2167 $self->cust_main->agent_template;
2170 sub _agent_invoice_from {
2172 $self->cust_main->agent_invoice_from;
2175 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2177 Returns an text invoice, as a list of lines.
2179 Options can be passed as a hashref (recommended) or as a list of time, template
2180 and then any key/value pairs for any other options.
2182 I<time>, if specified, is used to control the printing of overdue messages. The
2183 default is now. It isn't the date of the invoice; that's the `_date' field.
2184 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2185 L<Time::Local> and L<Date::Parse> for conversion functions.
2187 I<template>, if specified, is the name of a suffix for alternate invoices.
2189 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2195 my( $today, $template, %opt );
2197 %opt = %{ shift() };
2198 $today = delete($opt{'time'}) || '';
2199 $template = delete($opt{template}) || '';
2201 ( $today, $template, %opt ) = @_;
2204 my %params = ( 'format' => 'template' );
2205 $params{'time'} = $today if $today;
2206 $params{'template'} = $template if $template;
2207 $params{$_} = $opt{$_}
2208 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2210 $self->print_generic( %params );
2213 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2215 Internal method - returns a filename of a filled-in LaTeX template for this
2216 invoice (Note: add ".tex" to get the actual filename), and a filename of
2217 an associated logo (with the .eps extension included).
2219 See print_ps and print_pdf for methods that return PostScript and PDF output.
2221 Options can be passed as a hashref (recommended) or as a list of time, template
2222 and then any key/value pairs for any other options.
2224 I<time>, if specified, is used to control the printing of overdue messages. The
2225 default is now. It isn't the date of the invoice; that's the `_date' field.
2226 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2227 L<Time::Local> and L<Date::Parse> for conversion functions.
2229 I<template>, if specified, is the name of a suffix for alternate invoices.
2231 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2237 my $conf = $self->conf;
2238 my( $today, $template, %opt );
2240 %opt = %{ shift() };
2241 $today = delete($opt{'time'}) || '';
2242 $template = delete($opt{template}) || '';
2244 ( $today, $template, %opt ) = @_;
2247 my %params = ( 'format' => 'latex' );
2248 $params{'time'} = $today if $today;
2249 $params{'template'} = $template if $template;
2250 $params{$_} = $opt{$_}
2251 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2253 $template ||= $self->_agent_template;
2255 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2256 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2260 ) or die "can't open temp file: $!\n";
2262 my $agentnum = $self->cust_main->agentnum;
2264 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2265 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2266 or die "can't write temp file: $!\n";
2268 print $lh $conf->config_binary('logo.eps', $agentnum)
2269 or die "can't write temp file: $!\n";
2272 $params{'logo_file'} = $lh->filename;
2274 if($conf->exists('invoice-barcode')){
2275 my $png_file = $self->invoice_barcode($dir);
2276 my $eps_file = $png_file;
2277 $eps_file =~ s/\.png$/.eps/g;
2278 $png_file =~ /(barcode.*png)/;
2280 $eps_file =~ /(barcode.*eps)/;
2283 my $curr_dir = cwd();
2285 # after painfuly long experimentation, it was determined that sam2p won't
2286 # accept : and other chars in the path, no matter how hard I tried to
2287 # escape them, hence the chdir (and chdir back, just to be safe)
2288 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2289 or die "sam2p failed: $!\n";
2293 $params{'barcode_file'} = $eps_file;
2296 my @filled_in = $self->print_generic( %params );
2298 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2302 ) or die "can't open temp file: $!\n";
2303 binmode($fh, ':utf8'); # language support
2304 print $fh join('', @filled_in );
2307 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2308 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2312 =item invoice_barcode DIR_OR_FALSE
2314 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2315 it is taken as the temp directory where the PNG file will be generated and the
2316 PNG file name is returned. Otherwise, the PNG image itself is returned.
2320 sub invoice_barcode {
2321 my ($self, $dir) = (shift,shift);
2323 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2324 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2325 my $gd = $gdbar->plot(Height => 30);
2328 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2332 ) or die "can't open temp file: $!\n";
2333 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2334 my $png_file = $bh->filename;
2341 =item print_generic OPTION => VALUE ...
2343 Internal method - returns a filled-in template for this invoice as a scalar.
2345 See print_ps and print_pdf for methods that return PostScript and PDF output.
2347 Non optional options include
2348 format - latex, html, template
2350 Optional options include
2352 template - a value used as a suffix for a configuration template
2354 time - a value used to control the printing of overdue messages. The
2355 default is now. It isn't the date of the invoice; that's the `_date' field.
2356 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2357 L<Time::Local> and L<Date::Parse> for conversion functions.
2361 unsquelch_cdr - overrides any per customer cdr squelching when true
2363 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2367 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2368 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2369 # yes: fixed width (dot matrix) text printing will be borked
2371 my( $self, %params ) = @_;
2372 my $conf = $self->conf;
2373 my $today = $params{today} ? $params{today} : time;
2374 warn "$me print_generic called on $self with suffix $params{template}\n"
2377 my $format = $params{format};
2378 die "Unknown format: $format"
2379 unless $format =~ /^(latex|html|template)$/;
2381 my $cust_main = $self->cust_main;
2382 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2383 unless $cust_main->payname
2384 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2386 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2387 'html' => [ '<%=', '%>' ],
2388 'template' => [ '{', '}' ],
2391 warn "$me print_generic creating template\n"
2394 #create the template
2395 my $template = $params{template} ? $params{template} : $self->_agent_template;
2396 my $templatefile = "invoice_$format";
2397 $templatefile .= "_$template"
2398 if length($template) && $conf->exists($templatefile."_$template");
2399 my @invoice_template = map "$_\n", $conf->config($templatefile)
2400 or die "cannot load config data $templatefile";
2403 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2404 #change this to a die when the old code is removed
2405 warn "old-style invoice template $templatefile; ".
2406 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2407 $old_latex = 'true';
2408 @invoice_template = _translate_old_latex_format(@invoice_template);
2411 warn "$me print_generic creating T:T object\n"
2414 my $text_template = new Text::Template(
2416 SOURCE => \@invoice_template,
2417 DELIMITERS => $delimiters{$format},
2420 warn "$me print_generic compiling T:T object\n"
2423 $text_template->compile()
2424 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2427 # additional substitution could possibly cause breakage in existing templates
2428 my %convert_maps = (
2430 'notes' => sub { map "$_", @_ },
2431 'footer' => sub { map "$_", @_ },
2432 'smallfooter' => sub { map "$_", @_ },
2433 'returnaddress' => sub { map "$_", @_ },
2434 'coupon' => sub { map "$_", @_ },
2435 'summary' => sub { map "$_", @_ },
2441 s/%%(.*)$/<!-- $1 -->/g;
2442 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2443 s/\\begin\{enumerate\}/<ol>/g;
2445 s/\\end\{enumerate\}/<\/ol>/g;
2446 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2455 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2457 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2462 s/\\\\\*?\s*$/<BR>/;
2463 s/\\hyphenation\{[\w\s\-]+}//;
2468 'coupon' => sub { "" },
2469 'summary' => sub { "" },
2476 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2477 s/\\begin\{enumerate\}//g;
2479 s/\\end\{enumerate\}//g;
2480 s/\\textbf\{(.*)\}/$1/g;
2487 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2489 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2494 s/\\\\\*?\s*$/\n/; # dubious
2495 s/\\hyphenation\{[\w\s\-]+}//;
2499 'coupon' => sub { "" },
2500 'summary' => sub { "" },
2505 # hashes for differing output formats
2506 my %nbsps = ( 'latex' => '~',
2507 'html' => '', # '&nbps;' would be nice
2508 'template' => '', # not used
2510 my $nbsp = $nbsps{$format};
2512 my %escape_functions = ( 'latex' => \&_latex_escape,
2513 'html' => \&_html_escape_nbsp,#\&encode_entities,
2514 'template' => sub { shift },
2516 my $escape_function = $escape_functions{$format};
2517 my $escape_function_nonbsp = ($format eq 'html')
2518 ? \&_html_escape : $escape_function;
2520 my %date_formats = ( 'latex' => $date_format_long,
2521 'html' => $date_format_long,
2524 $date_formats{'html'} =~ s/ / /g;
2526 my $date_format = $date_formats{$format};
2528 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2530 'html' => sub { return '<b>'. shift(). '</b>'
2532 'template' => sub { shift },
2534 my $embolden_function = $embolden_functions{$format};
2536 my %newline_tokens = ( 'latex' => '\\\\',
2540 my $newline_token = $newline_tokens{$format};
2542 warn "$me generating template variables\n"
2545 # generate template variables
2548 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2552 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2558 $returnaddress = join("\n",
2559 $conf->config_orbase("invoice_${format}returnaddress", $template)
2562 } elsif ( grep /\S/,
2563 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2565 my $convert_map = $convert_maps{$format}{'returnaddress'};
2568 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2573 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2575 my $convert_map = $convert_maps{$format}{'returnaddress'};
2576 $returnaddress = join( "\n", &$convert_map(
2577 map { s/( {2,})/'~' x length($1)/eg;
2581 ( $conf->config('company_name', $self->cust_main->agentnum),
2582 $conf->config('company_address', $self->cust_main->agentnum),
2589 my $warning = "Couldn't find a return address; ".
2590 "do you need to set the company_address configuration value?";
2592 $returnaddress = $nbsp;
2593 #$returnaddress = $warning;
2597 warn "$me generating invoice data\n"
2600 my $agentnum = $self->cust_main->agentnum;
2602 my %invoice_data = (
2605 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2606 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2607 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2608 'returnaddress' => $returnaddress,
2609 'agent' => &$escape_function($cust_main->agent->agent),
2612 'invnum' => $self->invnum,
2613 'date' => time2str($date_format, $self->_date),
2614 'today' => time2str($date_format_long, $today),
2615 'terms' => $self->terms,
2616 'template' => $template, #params{'template'},
2617 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2618 'current_charges' => sprintf("%.2f", $self->charged),
2619 'duedate' => $self->due_date2str($rdate_format), #date_format?
2622 'custnum' => $cust_main->display_custnum,
2623 'agent_custid' => &$escape_function($cust_main->agent_custid),
2624 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2625 payname company address1 address2 city state zip fax
2629 'ship_enable' => $conf->exists('invoice-ship_address'),
2630 'unitprices' => $conf->exists('invoice-unitprice'),
2631 'smallernotes' => $conf->exists('invoice-smallernotes'),
2632 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2633 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2635 #layout info -- would be fancy to calc some of this and bury the template
2637 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2638 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2639 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2640 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2641 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2642 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2643 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2644 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2645 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2646 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2648 # better hang on to conf_dir for a while (for old templates)
2649 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2651 #these are only used when doing paged plaintext
2658 my $lh = FS::L10N->get_handle($cust_main->locale);
2659 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2661 my $min_sdate = 999999999999;
2663 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2664 next unless $cust_bill_pkg->pkgnum > 0;
2665 $min_sdate = $cust_bill_pkg->sdate
2666 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2667 $max_edate = $cust_bill_pkg->edate
2668 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2671 $invoice_data{'bill_period'} = '';
2672 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2673 . " to " . time2str('%e %h', $max_edate)
2674 if ($max_edate != 0 && $min_sdate != 999999999999);
2676 $invoice_data{finance_section} = '';
2677 if ( $conf->config('finance_pkgclass') ) {
2679 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2680 $invoice_data{finance_section} = $pkg_class->categoryname;
2682 $invoice_data{finance_amount} = '0.00';
2683 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2685 my $countrydefault = $conf->config('countrydefault') || 'US';
2686 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2687 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2688 my $method = $prefix.$_;
2689 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2691 $invoice_data{'ship_country'} = ''
2692 if ( $invoice_data{'ship_country'} eq $countrydefault );
2694 $invoice_data{'cid'} = $params{'cid'}
2697 if ( $cust_main->country eq $countrydefault ) {
2698 $invoice_data{'country'} = '';
2700 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2704 $invoice_data{'address'} = \@address;
2706 $cust_main->payname.
2707 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2708 ? " (P.O. #". $cust_main->payinfo. ")"
2712 push @address, $cust_main->company
2713 if $cust_main->company;
2714 push @address, $cust_main->address1;
2715 push @address, $cust_main->address2
2716 if $cust_main->address2;
2718 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2719 push @address, $invoice_data{'country'}
2720 if $invoice_data{'country'};
2722 while (scalar(@address) < 5);
2724 $invoice_data{'logo_file'} = $params{'logo_file'}
2725 if $params{'logo_file'};
2726 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2727 if $params{'barcode_file'};
2728 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2729 if $params{'barcode_img'};
2730 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2731 if $params{'barcode_cid'};
2733 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2734 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2735 #my $balance_due = $self->owed + $pr_total - $cr_total;
2736 my $balance_due = $self->owed + $pr_total;
2737 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2738 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2739 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2740 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2742 my $summarypage = '';
2743 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2746 $invoice_data{'summarypage'} = $summarypage;
2748 warn "$me substituting variables in notes, footer, smallfooter\n"
2751 my @include = (qw( notes footer smallfooter ));
2752 push @include, 'coupon' unless $params{'no_coupon'};
2753 foreach my $include (@include) {
2755 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2758 if ( $conf->exists($inc_file, $agentnum)
2759 && length( $conf->config($inc_file, $agentnum) ) ) {
2761 @inc_src = $conf->config($inc_file, $agentnum);
2765 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2767 my $convert_map = $convert_maps{$format}{$include};
2769 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2770 s/--\@\]/$delimiters{$format}[1]/g;
2773 &$convert_map( $conf->config($inc_file, $agentnum) );
2777 my $inc_tt = new Text::Template (
2779 SOURCE => [ map "$_\n", @inc_src ],
2780 DELIMITERS => $delimiters{$format},
2781 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2783 unless ( $inc_tt->compile() ) {
2784 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2785 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2789 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2791 $invoice_data{$include} =~ s/\n+$//
2792 if ($format eq 'latex');
2795 $invoice_data{'po_line'} =
2796 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2797 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2800 my %money_chars = ( 'latex' => '',
2801 'html' => $conf->config('money_char') || '$',
2804 my $money_char = $money_chars{$format};
2806 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2807 'html' => $conf->config('money_char') || '$',
2810 my $other_money_char = $other_money_chars{$format};
2811 $invoice_data{'dollar'} = $other_money_char;
2813 my @detail_items = ();
2814 my @total_items = ();
2818 $invoice_data{'detail_items'} = \@detail_items;
2819 $invoice_data{'total_items'} = \@total_items;
2820 $invoice_data{'buf'} = \@buf;
2821 $invoice_data{'sections'} = \@sections;
2823 warn "$me generating sections\n"
2826 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2827 'subtotal' => $other_money_char.
2828 sprintf('%.2f', $pr_total),
2829 'summarized' => $summarypage ? 'Y' : '',
2831 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2832 join(' / ', map { $cust_main->balance_date_range(@$_) }
2833 $self->_prior_month30s
2835 if $conf->exists('invoice_include_aging');
2838 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2839 'subtotal' => $taxtotal, # adjusted below
2840 'summarized' => $summarypage ? 'Y' : '',
2842 my $tax_weight = _pkg_category($tax_section->{description})
2843 ? _pkg_category($tax_section->{description})->weight
2845 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2846 $tax_section->{'sort_weight'} = $tax_weight;
2849 my $adjusttotal = 0;
2850 my $adjust_section = { 'description' =>
2851 $self->mt('Credits, Payments, and Adjustments'),
2852 'subtotal' => 0, # adjusted below
2853 'summarized' => $summarypage ? 'Y' : '',
2855 my $adjust_weight = _pkg_category($adjust_section->{description})
2856 ? _pkg_category($adjust_section->{description})->weight
2858 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2859 $adjust_section->{'sort_weight'} = $adjust_weight;
2861 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2862 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2863 $invoice_data{'multisection'} = $multisection;
2864 my $late_sections = [];
2865 my $extra_sections = [];
2866 my $extra_lines = ();
2867 if ( $multisection ) {
2868 ($extra_sections, $extra_lines) =
2869 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2870 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2872 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2874 push @detail_items, @$extra_lines if $extra_lines;
2876 $self->_items_sections( $late_sections, # this could stand a refactor
2878 $escape_function_nonbsp,
2882 if ($conf->exists('svc_phone_sections')) {
2883 my ($phone_sections, $phone_lines) =
2884 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2885 push @{$late_sections}, @$phone_sections;
2886 push @detail_items, @$phone_lines;
2888 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2889 my ($accountcode_section, $accountcode_lines) =
2890 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2891 if ( scalar(@$accountcode_lines) ) {
2892 push @{$late_sections}, $accountcode_section;
2893 push @detail_items, @$accountcode_lines;
2897 push @sections, { 'description' => '', 'subtotal' => '' };
2900 unless ( $conf->exists('disable_previous_balance')
2901 || $conf->exists('previous_balance-summary_only')
2905 warn "$me adding previous balances\n"
2908 foreach my $line_item ( $self->_items_previous ) {
2911 ext_description => [],
2913 $detail->{'ref'} = $line_item->{'pkgnum'};
2914 $detail->{'quantity'} = 1;
2915 $detail->{'section'} = $previous_section;
2916 $detail->{'description'} = &$escape_function($line_item->{'description'});
2917 if ( exists $line_item->{'ext_description'} ) {
2918 @{$detail->{'ext_description'}} = map {
2919 &$escape_function($_);
2920 } @{$line_item->{'ext_description'}};
2922 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2923 $line_item->{'amount'};
2924 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2926 push @detail_items, $detail;
2927 push @buf, [ $detail->{'description'},
2928 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2934 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2935 push @buf, ['','-----------'];
2936 push @buf, [ $self->mt('Total Previous Balance'),
2937 $money_char. sprintf("%10.2f", $pr_total) ];
2941 if ( $conf->exists('svc_phone-did-summary') ) {
2942 warn "$me adding DID summary\n"
2945 my ($didsummary,$minutes) = $self->_did_summary;
2946 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2948 { 'description' => $didsummary_desc,
2949 'ext_description' => [ $didsummary, $minutes ],
2953 foreach my $section (@sections, @$late_sections) {
2955 warn "$me adding section \n". Dumper($section)
2958 # begin some normalization
2959 $section->{'subtotal'} = $section->{'amount'}
2961 && !exists($section->{subtotal})
2962 && exists($section->{amount});
2964 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2965 if ( $invoice_data{finance_section} &&
2966 $section->{'description'} eq $invoice_data{finance_section} );
2968 $section->{'subtotal'} = $other_money_char.
2969 sprintf('%.2f', $section->{'subtotal'})
2972 # continue some normalization
2973 $section->{'amount'} = $section->{'subtotal'}
2977 if ( $section->{'description'} ) {
2978 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2983 warn "$me setting options\n"
2986 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2988 $options{'section'} = $section if $multisection;
2989 $options{'format'} = $format;
2990 $options{'escape_function'} = $escape_function;
2991 $options{'format_function'} = sub { () } unless $unsquelched;
2992 $options{'unsquelched'} = $unsquelched;
2993 $options{'summary_page'} = $summarypage;
2994 $options{'skip_usage'} =
2995 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2996 $options{'multilocation'} = $multilocation;
2997 $options{'multisection'} = $multisection;
2999 warn "$me searching for line items\n"
3002 foreach my $line_item ( $self->_items_pkg(%options) ) {
3004 warn "$me adding line item $line_item\n"
3008 ext_description => [],
3010 $detail->{'ref'} = $line_item->{'pkgnum'};
3011 $detail->{'quantity'} = $line_item->{'quantity'};
3012 $detail->{'section'} = $section;
3013 $detail->{'description'} = &$escape_function($line_item->{'description'});
3014 if ( exists $line_item->{'ext_description'} ) {
3015 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3017 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3018 $line_item->{'amount'};
3019 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3020 $line_item->{'unit_amount'};
3021 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3023 push @detail_items, $detail;
3024 push @buf, ( [ $detail->{'description'},
3025 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3027 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3031 if ( $section->{'description'} ) {
3032 push @buf, ( ['','-----------'],
3033 [ $section->{'description'}. ' sub-total',
3034 $money_char. sprintf("%10.2f", $section->{'subtotal'})
3043 $invoice_data{current_less_finance} =
3044 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3046 if ( $multisection && !$conf->exists('disable_previous_balance')
3047 || $conf->exists('previous_balance-summary_only') )
3049 unshift @sections, $previous_section if $pr_total;
3052 warn "$me adding taxes\n"
3055 foreach my $tax ( $self->_items_tax ) {
3057 $taxtotal += $tax->{'amount'};
3059 my $description = &$escape_function( $tax->{'description'} );
3060 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3062 if ( $multisection ) {
3064 my $money = $old_latex ? '' : $money_char;
3065 push @detail_items, {
3066 ext_description => [],
3069 description => $description,
3070 amount => $money. $amount,
3072 section => $tax_section,
3077 push @total_items, {
3078 'total_item' => $description,
3079 'total_amount' => $other_money_char. $amount,
3084 push @buf,[ $description,
3085 $money_char. $amount,
3092 $total->{'total_item'} = $self->mt('Sub-total');
3093 $total->{'total_amount'} =
3094 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3096 if ( $multisection ) {
3097 $tax_section->{'subtotal'} = $other_money_char.
3098 sprintf('%.2f', $taxtotal);
3099 $tax_section->{'pretotal'} = 'New charges sub-total '.
3100 $total->{'total_amount'};
3101 push @sections, $tax_section if $taxtotal;
3103 unshift @total_items, $total;
3106 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3108 push @buf,['','-----------'];
3109 push @buf,[$self->mt(
3110 $conf->exists('disable_previous_balance')
3112 : 'Total New Charges'
3114 $money_char. sprintf("%10.2f",$self->charged) ];
3119 my $item = $self->mt('Total');
3120 $item = $conf->config('previous_balance-exclude_from_total')
3121 || 'Total New Charges'
3122 if $conf->exists('previous_balance-exclude_from_total');
3123 my $amount = $self->charged +
3124 ( $conf->exists('disable_previous_balance') ||
3125 $conf->exists('previous_balance-exclude_from_total')
3129 $total->{'total_item'} = &$embolden_function($item);
3130 $total->{'total_amount'} =
3131 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3132 if ( $multisection ) {
3133 if ( $adjust_section->{'sort_weight'} ) {
3134 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3135 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3137 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3138 $other_money_char. sprintf('%.2f', $self->charged );
3141 push @total_items, $total;
3143 push @buf,['','-----------'];
3146 sprintf( '%10.2f', $amount )
3151 unless ( $conf->exists('disable_previous_balance') ) {
3152 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3155 my $credittotal = 0;
3156 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3159 $total->{'total_item'} = &$escape_function($credit->{'description'});
3160 $credittotal += $credit->{'amount'};
3161 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3162 $adjusttotal += $credit->{'amount'};
3163 if ( $multisection ) {
3164 my $money = $old_latex ? '' : $money_char;
3165 push @detail_items, {
3166 ext_description => [],
3169 description => &$escape_function($credit->{'description'}),
3170 amount => $money. $credit->{'amount'},
3172 section => $adjust_section,
3175 push @total_items, $total;
3179 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3182 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3183 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3187 my $paymenttotal = 0;
3188 foreach my $payment ( $self->_items_payments ) {
3190 $total->{'total_item'} = &$escape_function($payment->{'description'});
3191 $paymenttotal += $payment->{'amount'};
3192 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3193 $adjusttotal += $payment->{'amount'};
3194 if ( $multisection ) {
3195 my $money = $old_latex ? '' : $money_char;
3196 push @detail_items, {
3197 ext_description => [],
3200 description => &$escape_function($payment->{'description'}),
3201 amount => $money. $payment->{'amount'},
3203 section => $adjust_section,
3206 push @total_items, $total;
3208 push @buf, [ $payment->{'description'},
3209 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3212 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3214 if ( $multisection ) {
3215 $adjust_section->{'subtotal'} = $other_money_char.
3216 sprintf('%.2f', $adjusttotal);
3217 push @sections, $adjust_section
3218 unless $adjust_section->{sort_weight};
3223 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3224 $total->{'total_amount'} =
3225 &$embolden_function(
3226 $other_money_char. sprintf('%.2f', $summarypage
3228 $self->billing_balance
3229 : $self->owed + $pr_total
3232 if ( $multisection && !$adjust_section->{sort_weight} ) {
3233 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3234 $total->{'total_amount'};
3236 push @total_items, $total;
3238 push @buf,['','-----------'];
3239 push @buf,[$self->balance_due_msg, $money_char.
3240 sprintf("%10.2f", $balance_due ) ];
3243 if ( $conf->exists('previous_balance-show_credit')
3244 and $cust_main->balance < 0 ) {
3245 my $credit_total = {
3246 'total_item' => &$embolden_function($self->credit_balance_msg),
3247 'total_amount' => &$embolden_function(
3248 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3251 if ( $multisection ) {
3252 $adjust_section->{'posttotal'} .= $newline_token .
3253 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3256 push @total_items, $credit_total;
3258 push @buf,['','-----------'];
3259 push @buf,[$self->credit_balance_msg, $money_char.
3260 sprintf("%10.2f", -$cust_main->balance ) ];
3264 if ( $multisection ) {
3265 if ($conf->exists('svc_phone_sections')) {
3267 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3268 $total->{'total_amount'} =
3269 &$embolden_function(
3270 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3272 my $last_section = pop @sections;
3273 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3274 $total->{'total_amount'};
3275 push @sections, $last_section;
3277 push @sections, @$late_sections
3281 my @includelist = ();
3282 push @includelist, 'summary' if $summarypage;
3283 foreach my $include ( @includelist ) {
3285 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3288 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3290 @inc_src = $conf->config($inc_file, $agentnum);
3294 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3296 my $convert_map = $convert_maps{$format}{$include};
3298 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3299 s/--\@\]/$delimiters{$format}[1]/g;
3302 &$convert_map( $conf->config($inc_file, $agentnum) );
3306 my $inc_tt = new Text::Template (
3308 SOURCE => [ map "$_\n", @inc_src ],
3309 DELIMITERS => $delimiters{$format},
3310 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3312 unless ( $inc_tt->compile() ) {
3313 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3314 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3318 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3320 $invoice_data{$include} =~ s/\n+$//
3321 if ($format eq 'latex');
3326 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3327 /invoice_lines\((\d*)\)/;
3328 $invoice_lines += $1 || scalar(@buf);
3331 die "no invoice_lines() functions in template?"
3332 if ( $format eq 'template' && !$wasfunc );
3334 if ($format eq 'template') {
3336 if ( $invoice_lines ) {
3337 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3338 $invoice_data{'total_pages'}++
3339 if scalar(@buf) % $invoice_lines;
3342 #setup subroutine for the template
3343 #sub FS::cust_bill::_template::invoice_lines { # good god, no
3344 $invoice_data{invoice_lines} = sub { # much better
3345 my $lines = shift || scalar(@buf);
3357 push @collect, split("\n",
3358 $text_template->fill_in( HASH => \%invoice_data )
3360 $invoice_data{'page'}++;
3362 map "$_\n", @collect;
3364 warn "filling in template for invoice ". $self->invnum. "\n"
3366 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3369 $text_template->fill_in(HASH => \%invoice_data);
3373 # helper routine for generating date ranges
3374 sub _prior_month30s {
3377 [ 1, 2592000 ], # 0-30 days ago
3378 [ 2592000, 5184000 ], # 30-60 days ago
3379 [ 5184000, 7776000 ], # 60-90 days ago
3380 [ 7776000, 0 ], # 90+ days ago
3383 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3384 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3389 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3391 Returns an postscript invoice, as a scalar.
3393 Options can be passed as a hashref (recommended) or as a list of time, template
3394 and then any key/value pairs for any other options.
3396 I<time> an optional value used to control the printing of overdue messages. The
3397 default is now. It isn't the date of the invoice; that's the `_date' field.
3398 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3399 L<Time::Local> and L<Date::Parse> for conversion functions.
3401 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3408 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3409 my $ps = generate_ps($file);
3411 unlink($barcodefile) if $barcodefile;
3416 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3418 Returns an PDF invoice, as a scalar.
3420 Options can be passed as a hashref (recommended) or as a list of time, template
3421 and then any key/value pairs for any other options.
3423 I<time> an optional value used to control the printing of overdue messages. The
3424 default is now. It isn't the date of the invoice; that's the `_date' field.
3425 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3426 L<Time::Local> and L<Date::Parse> for conversion functions.
3428 I<template>, if specified, is the name of a suffix for alternate invoices.
3430 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3437 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3438 my $pdf = generate_pdf($file);
3440 unlink($barcodefile) if $barcodefile;
3445 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3447 Returns an HTML invoice, as a scalar.
3449 I<time> an optional value used to control the printing of overdue messages. The
3450 default is now. It isn't the date of the invoice; that's the `_date' field.
3451 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3452 L<Time::Local> and L<Date::Parse> for conversion functions.
3454 I<template>, if specified, is the name of a suffix for alternate invoices.
3456 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3458 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3459 when emailing the invoice as part of a multipart/related MIME email.
3467 %params = %{ shift() };
3469 $params{'time'} = shift;
3470 $params{'template'} = shift;
3471 $params{'cid'} = shift;
3474 $params{'format'} = 'html';
3476 $self->print_generic( %params );
3479 # quick subroutine for print_latex
3481 # There are ten characters that LaTeX treats as special characters, which
3482 # means that they do not simply typeset themselves:
3483 # # $ % & ~ _ ^ \ { }
3485 # TeX ignores blanks following an escaped character; if you want a blank (as
3486 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3490 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3491 $value =~ s/([<>])/\$$1\$/g;
3497 encode_entities($value);
3501 sub _html_escape_nbsp {
3502 my $value = _html_escape(shift);
3503 $value =~ s/ +/ /g;
3507 #utility methods for print_*
3509 sub _translate_old_latex_format {
3510 warn "_translate_old_latex_format called\n"
3517 if ( $line =~ /^%%Detail\s*$/ ) {
3519 push @template, q![@--!,
3520 q! foreach my $_tr_line (@detail_items) {!,
3521 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3522 q! $_tr_line->{'description'} .= !,
3523 q! "\\tabularnewline\n~~".!,
3524 q! join( "\\tabularnewline\n~~",!,
3525 q! @{$_tr_line->{'ext_description'}}!,
3529 while ( ( my $line_item_line = shift )
3530 !~ /^%%EndDetail\s*$/ ) {
3531 $line_item_line =~ s/'/\\'/g; # nice LTS
3532 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3533 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3534 push @template, " \$OUT .= '$line_item_line';";
3537 push @template, '}',
3540 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3542 push @template, '[@--',
3543 ' foreach my $_tr_line (@total_items) {';
3545 while ( ( my $total_item_line = shift )
3546 !~ /^%%EndTotalDetails\s*$/ ) {
3547 $total_item_line =~ s/'/\\'/g; # nice LTS
3548 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3549 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3550 push @template, " \$OUT .= '$total_item_line';";
3553 push @template, '}',
3557 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3558 push @template, $line;
3564 warn "$_\n" foreach @template;
3572 my $conf = $self->conf;
3574 #check for an invoice-specific override
3575 return $self->invoice_terms if $self->invoice_terms;
3577 #check for a customer- specific override
3578 my $cust_main = $self->cust_main;
3579 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3581 #use configured default
3582 $conf->config('invoice_default_terms') || '';
3588 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3589 $duedate = $self->_date() + ( $1 * 86400 );
3596 $self->due_date ? time2str(shift, $self->due_date) : '';
3599 sub balance_due_msg {
3601 my $msg = $self->mt('Balance Due');
3602 return $msg unless $self->terms;
3603 if ( $self->due_date ) {
3604 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3605 $self->due_date2str($date_format);
3606 } elsif ( $self->terms ) {
3607 $msg .= ' - '. $self->terms;
3612 sub balance_due_date {
3614 my $conf = $self->conf;
3616 if ( $conf->exists('invoice_default_terms')
3617 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3618 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3623 sub credit_balance_msg {
3625 $self->mt('Credit Balance Remaining')
3628 =item invnum_date_pretty
3630 Returns a string with the invoice number and date, for example:
3631 "Invoice #54 (3/20/2008)"
3635 sub invnum_date_pretty {
3637 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3642 Returns a string with the date, for example: "3/20/2008"
3648 time2str($date_format, $self->_date);
3651 use vars qw(%pkg_category_cache);
3652 sub _items_sections {
3655 my $summarypage = shift;
3657 my $extra_sections = shift;
3661 my %late_subtotal = ();
3664 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3667 my $usage = $cust_bill_pkg->usage;
3669 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3670 next if ( $display->summary && $summarypage );
3672 my $section = $display->section;
3673 my $type = $display->type;
3675 $not_tax{$section} = 1
3676 unless $cust_bill_pkg->pkgnum == 0;
3678 if ( $display->post_total && !$summarypage ) {
3679 if (! $type || $type eq 'S') {
3680 $late_subtotal{$section} += $cust_bill_pkg->setup
3681 if $cust_bill_pkg->setup != 0;
3685 $late_subtotal{$section} += $cust_bill_pkg->recur
3686 if $cust_bill_pkg->recur != 0;
3689 if ($type && $type eq 'R') {
3690 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3691 if $cust_bill_pkg->recur != 0;
3694 if ($type && $type eq 'U') {
3695 $late_subtotal{$section} += $usage
3696 unless scalar(@$extra_sections);
3701 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3703 if (! $type || $type eq 'S') {
3704 $subtotal{$section} += $cust_bill_pkg->setup
3705 if $cust_bill_pkg->setup != 0;
3709 $subtotal{$section} += $cust_bill_pkg->recur
3710 if $cust_bill_pkg->recur != 0;
3713 if ($type && $type eq 'R') {
3714 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3715 if $cust_bill_pkg->recur != 0;
3718 if ($type && $type eq 'U') {
3719 $subtotal{$section} += $usage
3720 unless scalar(@$extra_sections);
3729 %pkg_category_cache = ();
3731 push @$late, map { { 'description' => &{$escape}($_),
3732 'subtotal' => $late_subtotal{$_},
3734 'sort_weight' => ( _pkg_category($_)
3735 ? _pkg_category($_)->weight
3738 ((_pkg_category($_) && _pkg_category($_)->condense)
3739 ? $self->_condense_section($format)
3743 sort _sectionsort keys %late_subtotal;
3746 if ( $summarypage ) {
3747 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3748 map { $_->categoryname } qsearch('pkg_category', {});
3749 push @sections, '' if exists($subtotal{''});
3751 @sections = keys %subtotal;
3754 my @early = map { { 'description' => &{$escape}($_),
3755 'subtotal' => $subtotal{$_},
3756 'summarized' => $not_tax{$_} ? '' : 'Y',
3757 'tax_section' => $not_tax{$_} ? '' : 'Y',
3758 'sort_weight' => ( _pkg_category($_)
3759 ? _pkg_category($_)->weight
3762 ((_pkg_category($_) && _pkg_category($_)->condense)
3763 ? $self->_condense_section($format)
3768 push @early, @$extra_sections if $extra_sections;
3770 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3774 #helper subs for above
3777 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3781 my $categoryname = shift;
3782 $pkg_category_cache{$categoryname} ||=
3783 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3786 my %condensed_format = (
3787 'label' => [ qw( Description Qty Amount ) ],
3789 sub { shift->{description} },
3790 sub { shift->{quantity} },
3791 sub { my($href, %opt) = @_;
3792 ($opt{dollar} || ''). $href->{amount};
3795 'align' => [ qw( l r r ) ],
3796 'span' => [ qw( 5 1 1 ) ], # unitprices?
3797 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3800 sub _condense_section {
3801 my ( $self, $format ) = ( shift, shift );
3803 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3804 qw( description_generator
3807 total_line_generator
3812 sub _condensed_generator_defaults {
3813 my ( $self, $format ) = ( shift, shift );
3814 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3823 sub _condensed_header_generator {
3824 my ( $self, $format ) = ( shift, shift );
3826 my ( $f, $prefix, $suffix, $separator, $column ) =
3827 _condensed_generator_defaults($format);
3829 if ($format eq 'latex') {
3830 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3831 $suffix = "\\\\\n\\hline";
3834 sub { my ($d,$a,$s,$w) = @_;
3835 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3837 } elsif ( $format eq 'html' ) {
3838 $prefix = '<th></th>';
3842 sub { my ($d,$a,$s,$w) = @_;
3843 return qq!<th align="$html_align{$a}">$d</th>!;
3851 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3853 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3856 $prefix. join($separator, @result). $suffix;
3861 sub _condensed_description_generator {
3862 my ( $self, $format ) = ( shift, shift );
3864 my ( $f, $prefix, $suffix, $separator, $column ) =
3865 _condensed_generator_defaults($format);
3867 my $money_char = '$';
3868 if ($format eq 'latex') {
3869 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3871 $separator = " & \n";
3873 sub { my ($d,$a,$s,$w) = @_;
3874 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3876 $money_char = '\\dollar';
3877 }elsif ( $format eq 'html' ) {
3878 $prefix = '"><td align="center"></td>';
3882 sub { my ($d,$a,$s,$w) = @_;
3883 return qq!<td align="$html_align{$a}">$d</td>!;
3885 #$money_char = $conf->config('money_char') || '$';
3886 $money_char = ''; # this is madness
3894 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3896 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3898 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3899 map { $f->{$_}->[$i] } qw(align span width)
3903 $prefix. join( $separator, @result ). $suffix;
3908 sub _condensed_total_generator {
3909 my ( $self, $format ) = ( shift, shift );
3911 my ( $f, $prefix, $suffix, $separator, $column ) =
3912 _condensed_generator_defaults($format);
3915 if ($format eq 'latex') {
3918 $separator = " & \n";
3920 sub { my ($d,$a,$s,$w) = @_;
3921 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3923 }elsif ( $format eq 'html' ) {
3927 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3929 sub { my ($d,$a,$s,$w) = @_;
3930 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3939 # my $r = &{$f->{fields}->[$i]}(@args);
3940 # $r .= ' Total' unless $i;
3942 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3944 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3945 map { $f->{$_}->[$i] } qw(align span width)
3949 $prefix. join( $separator, @result ). $suffix;
3954 =item total_line_generator FORMAT
3956 Returns a coderef used for generation of invoice total line items for this
3957 usage_class. FORMAT is either html or latex
3961 # should not be used: will have issues with hash element names (description vs
3962 # total_item and amount vs total_amount -- another array of functions?
3964 sub _condensed_total_line_generator {
3965 my ( $self, $format ) = ( shift, shift );
3967 my ( $f, $prefix, $suffix, $separator, $column ) =
3968 _condensed_generator_defaults($format);
3971 if ($format eq 'latex') {
3974 $separator = " & \n";
3976 sub { my ($d,$a,$s,$w) = @_;
3977 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3979 }elsif ( $format eq 'html' ) {
3983 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3985 sub { my ($d,$a,$s,$w) = @_;
3986 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3995 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3997 &{$column}( &{$f->{fields}->[$i]}(@args),
3998 map { $f->{$_}->[$i] } qw(align span width)
4002 $prefix. join( $separator, @result ). $suffix;
4007 #sub _items_extra_usage_sections {
4009 # my $escape = shift;
4011 # my %sections = ();
4013 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4014 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4016 # next unless $cust_bill_pkg->pkgnum > 0;
4018 # foreach my $section ( keys %usage_class ) {
4020 # my $usage = $cust_bill_pkg->usage($section);
4022 # next unless $usage && $usage > 0;
4024 # $sections{$section} ||= 0;
4025 # $sections{$section} += $usage;
4031 # map { { 'description' => &{$escape}($_),
4032 # 'subtotal' => $sections{$_},
4033 # 'summarized' => '',
4034 # 'tax_section' => '',
4037 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4041 sub _items_extra_usage_sections {
4043 my $conf = $self->conf;
4051 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4053 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4054 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4055 next unless $cust_bill_pkg->pkgnum > 0;
4057 foreach my $classnum ( keys %usage_class ) {
4058 my $section = $usage_class{$classnum}->classname;
4059 $classnums{$section} = $classnum;
4061 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4062 my $amount = $detail->amount;
4063 next unless $amount && $amount > 0;
4065 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4066 $sections{$section}{amount} += $amount; #subtotal
4067 $sections{$section}{calls}++;
4068 $sections{$section}{duration} += $detail->duration;
4070 my $desc = $detail->regionname;
4071 my $description = $desc;
4072 $description = substr($desc, 0, $maxlength). '...'
4073 if $format eq 'latex' && length($desc) > $maxlength;
4075 $lines{$section}{$desc} ||= {
4076 description => &{$escape}($description),
4077 #pkgpart => $part_pkg->pkgpart,
4078 pkgnum => $cust_bill_pkg->pkgnum,
4083 #unit_amount => $cust_bill_pkg->unitrecur,
4084 quantity => $cust_bill_pkg->quantity,
4085 product_code => 'N/A',
4086 ext_description => [],
4089 $lines{$section}{$desc}{amount} += $amount;
4090 $lines{$section}{$desc}{calls}++;
4091 $lines{$section}{$desc}{duration} += $detail->duration;
4097 my %sectionmap = ();
4098 foreach (keys %sections) {
4099 my $usage_class = $usage_class{$classnums{$_}};
4100 $sectionmap{$_} = { 'description' => &{$escape}($_),
4101 'amount' => $sections{$_}{amount}, #subtotal
4102 'calls' => $sections{$_}{calls},
4103 'duration' => $sections{$_}{duration},
4105 'tax_section' => '',
4106 'sort_weight' => $usage_class->weight,
4107 ( $usage_class->format
4108 ? ( map { $_ => $usage_class->$_($format) }
4109 qw( description_generator header_generator total_generator total_line_generator )
4116 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4120 foreach my $section ( keys %lines ) {
4121 foreach my $line ( keys %{$lines{$section}} ) {
4122 my $l = $lines{$section}{$line};
4123 $l->{section} = $sectionmap{$section};
4124 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4125 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4130 return(\@sections, \@lines);
4136 my $end = $self->_date;
4138 # start at date of previous invoice + 1 second or 0 if no previous invoice
4139 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4140 $start = 0 if !$start;
4143 my $cust_main = $self->cust_main;
4144 my @pkgs = $cust_main->all_pkgs;
4145 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4148 foreach my $pkg ( @pkgs ) {
4149 my @h_cust_svc = $pkg->h_cust_svc($end);
4150 foreach my $h_cust_svc ( @h_cust_svc ) {
4151 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4152 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4154 my $inserted = $h_cust_svc->date_inserted;
4155 my $deleted = $h_cust_svc->date_deleted;
4156 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4158 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4160 # DID either activated or ported in; cannot be both for same DID simultaneously
4161 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4162 && (!$phone_inserted->lnp_status
4163 || $phone_inserted->lnp_status eq ''
4164 || $phone_inserted->lnp_status eq 'native')) {
4167 else { # this one not so clean, should probably move to (h_)svc_phone
4168 my $phone_portedin = qsearchs( 'h_svc_phone',
4169 { 'svcnum' => $h_cust_svc->svcnum,
4170 'lnp_status' => 'portedin' },
4171 FS::h_svc_phone->sql_h_searchs($end),
4173 $num_portedin++ if $phone_portedin;
4176 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4177 if($deleted >= $start && $deleted <= $end && $phone_deleted
4178 && (!$phone_deleted->lnp_status
4179 || $phone_deleted->lnp_status ne 'portingout')) {
4182 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4183 && $phone_deleted->lnp_status
4184 && $phone_deleted->lnp_status eq 'portingout') {
4188 # increment usage minutes
4189 if ( $phone_inserted ) {
4190 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4191 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4194 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4197 # don't look at this service again
4198 push @seen, $h_cust_svc->svcnum;
4202 $minutes = sprintf("%d", $minutes);
4203 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4204 . "$num_deactivated Ported-Out: $num_portedout ",
4205 "Total Minutes: $minutes");
4208 sub _items_accountcode_cdr {
4213 my $section = { 'amount' => 0,
4216 'sort_weight' => '',
4218 'description' => 'Usage by Account Code',
4224 my %accountcodes = ();
4226 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4227 next unless $cust_bill_pkg->pkgnum > 0;
4229 my @header = $cust_bill_pkg->details_header;
4230 next unless scalar(@header);
4231 $section->{'header'} = join(',',@header);
4233 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4235 $section->{'header'} = $detail->formatted('format' => $format)
4236 if($detail->detail eq $section->{'header'});
4238 my $accountcode = $detail->accountcode;
4239 next unless $accountcode;
4241 my $amount = $detail->amount;
4242 next unless $amount && $amount > 0;
4244 $accountcodes{$accountcode} ||= {
4245 description => $accountcode,
4252 product_code => 'N/A',
4253 section => $section,
4254 ext_description => [ $section->{'header'} ],
4258 $section->{'amount'} += $amount;
4259 $accountcodes{$accountcode}{'amount'} += $amount;
4260 $accountcodes{$accountcode}{calls}++;
4261 $accountcodes{$accountcode}{duration} += $detail->duration;
4262 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4266 foreach my $l ( values %accountcodes ) {
4267 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4268 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4269 foreach my $sorted_detail ( @sorted_detail ) {
4270 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4272 delete $l->{detail_temp};
4276 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4278 return ($section,\@sorted_lines);
4281 sub _items_svc_phone_sections {
4283 my $conf = $self->conf;
4291 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4293 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4294 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4296 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4297 next unless $cust_bill_pkg->pkgnum > 0;
4299 my @header = $cust_bill_pkg->details_header;
4300 next unless scalar(@header);
4302 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4304 my $phonenum = $detail->phonenum;
4305 next unless $phonenum;
4307 my $amount = $detail->amount;
4308 next unless $amount && $amount > 0;
4310 $sections{$phonenum} ||= { 'amount' => 0,
4313 'sort_weight' => -1,
4314 'phonenum' => $phonenum,
4316 $sections{$phonenum}{amount} += $amount; #subtotal
4317 $sections{$phonenum}{calls}++;
4318 $sections{$phonenum}{duration} += $detail->duration;
4320 my $desc = $detail->regionname;
4321 my $description = $desc;
4322 $description = substr($desc, 0, $maxlength). '...'
4323 if $format eq 'latex' && length($desc) > $maxlength;
4325 $lines{$phonenum}{$desc} ||= {
4326 description => &{$escape}($description),
4327 #pkgpart => $part_pkg->pkgpart,
4335 product_code => 'N/A',
4336 ext_description => [],
4339 $lines{$phonenum}{$desc}{amount} += $amount;
4340 $lines{$phonenum}{$desc}{calls}++;
4341 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4343 my $line = $usage_class{$detail->classnum}->classname;
4344 $sections{"$phonenum $line"} ||=
4348 'sort_weight' => $usage_class{$detail->classnum}->weight,
4349 'phonenum' => $phonenum,
4350 'header' => [ @header ],
4352 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4353 $sections{"$phonenum $line"}{calls}++;
4354 $sections{"$phonenum $line"}{duration} += $detail->duration;
4356 $lines{"$phonenum $line"}{$desc} ||= {
4357 description => &{$escape}($description),
4358 #pkgpart => $part_pkg->pkgpart,
4366 product_code => 'N/A',
4367 ext_description => [],
4370 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4371 $lines{"$phonenum $line"}{$desc}{calls}++;
4372 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4373 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4374 $detail->formatted('format' => $format);
4379 my %sectionmap = ();
4380 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4381 foreach ( keys %sections ) {
4382 my @header = @{ $sections{$_}{header} || [] };
4384 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4385 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4386 my $usage_class = $summary ? $simple : $usage_simple;
4387 my $ending = $summary ? ' usage charges' : '';
4390 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4392 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4393 'amount' => $sections{$_}{amount}, #subtotal
4394 'calls' => $sections{$_}{calls},
4395 'duration' => $sections{$_}{duration},
4397 'tax_section' => '',
4398 'phonenum' => $sections{$_}{phonenum},
4399 'sort_weight' => $sections{$_}{sort_weight},
4400 'post_total' => $summary, #inspire pagebreak
4402 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4403 qw( description_generator
4406 total_line_generator
4413 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4414 $a->{sort_weight} <=> $b->{sort_weight}
4419 foreach my $section ( keys %lines ) {
4420 foreach my $line ( keys %{$lines{$section}} ) {
4421 my $l = $lines{$section}{$line};
4422 $l->{section} = $sectionmap{$section};
4423 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4424 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4429 if($conf->exists('phone_usage_class_summary')) {
4430 # this only works with Latex
4434 # after this, we'll have only two sections per DID:
4435 # Calls Summary and Calls Detail
4436 foreach my $section ( @sections ) {
4437 if($section->{'post_total'}) {
4438 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4439 $section->{'total_line_generator'} = sub { '' };
4440 $section->{'total_generator'} = sub { '' };
4441 $section->{'header_generator'} = sub { '' };
4442 $section->{'description_generator'} = '';
4443 push @newsections, $section;
4444 my %calls_detail = %$section;
4445 $calls_detail{'post_total'} = '';
4446 $calls_detail{'sort_weight'} = '';
4447 $calls_detail{'description_generator'} = sub { '' };
4448 $calls_detail{'header_generator'} = sub {
4449 return ' & Date/Time & Called Number & Duration & Price'
4450 if $format eq 'latex';
4453 $calls_detail{'description'} = 'Calls Detail: '
4454 . $section->{'phonenum'};
4455 push @newsections, \%calls_detail;
4459 # after this, each usage class is collapsed/summarized into a single
4460 # line under the Calls Summary section
4461 foreach my $newsection ( @newsections ) {
4462 if($newsection->{'post_total'}) { # this means Calls Summary
4463 foreach my $section ( @sections ) {
4464 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4465 && !$section->{'post_total'});
4466 my $newdesc = $section->{'description'};
4467 my $tn = $section->{'phonenum'};
4468 $newdesc =~ s/$tn//g;
4469 my $line = { ext_description => [],
4473 calls => $section->{'calls'},
4474 section => $newsection,
4475 duration => $section->{'duration'},
4476 description => $newdesc,
4477 amount => sprintf("%.2f",$section->{'amount'}),
4478 product_code => 'N/A',
4480 push @newlines, $line;
4485 # after this, Calls Details is populated with all CDRs
4486 foreach my $newsection ( @newsections ) {
4487 if(!$newsection->{'post_total'}) { # this means Calls Details
4488 foreach my $line ( @lines ) {
4489 next unless (scalar(@{$line->{'ext_description'}}) &&
4490 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4492 my @extdesc = @{$line->{'ext_description'}};
4494 foreach my $extdesc ( @extdesc ) {
4495 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4496 push @newextdesc, $extdesc;
4498 $line->{'ext_description'} = \@newextdesc;
4499 $line->{'section'} = $newsection;
4500 push @newlines, $line;
4505 return(\@newsections, \@newlines);
4508 return(\@sections, \@lines);
4515 #my @display = scalar(@_)
4517 # : qw( _items_previous _items_pkg );
4518 # #: qw( _items_pkg );
4519 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4520 my @display = qw( _items_previous _items_pkg );
4523 foreach my $display ( @display ) {
4524 push @b, $self->$display(@_);
4529 sub _items_previous {
4531 my $conf = $self->conf;
4532 my $cust_main = $self->cust_main;
4533 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4535 foreach ( @pr_cust_bill ) {
4536 my $date = $conf->exists('invoice_show_prior_due_date')
4537 ? 'due '. $_->due_date2str($date_format)
4538 : time2str($date_format, $_->_date);
4540 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4541 #'pkgpart' => 'N/A',
4543 'amount' => sprintf("%.2f", $_->owed),
4549 # 'description' => 'Previous Balance',
4550 # #'pkgpart' => 'N/A',
4551 # 'pkgnum' => 'N/A',
4552 # 'amount' => sprintf("%10.2f", $pr_total ),
4553 # 'ext_description' => [ map {
4554 # "Invoice ". $_->invnum.
4555 # " (". time2str("%x",$_->_date). ") ".
4556 # sprintf("%10.2f", $_->owed)
4557 # } @pr_cust_bill ],
4566 warn "$me _items_pkg searching for all package line items\n"
4569 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4571 warn "$me _items_pkg filtering line items\n"
4573 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4575 if ($options{section} && $options{section}->{condensed}) {
4577 warn "$me _items_pkg condensing section\n"
4581 local $Storable::canonical = 1;
4582 foreach ( @items ) {
4584 delete $item->{ref};
4585 delete $item->{ext_description};
4586 my $key = freeze($item);
4587 $itemshash{$key} ||= 0;
4588 $itemshash{$key} ++; # += $item->{quantity};
4590 @items = sort { $a->{description} cmp $b->{description} }
4591 map { my $i = thaw($_);
4592 $i->{quantity} = $itemshash{$_};
4594 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4600 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4607 return 0 unless $a->itemdesc cmp $b->itemdesc;
4608 return -1 if $b->itemdesc eq 'Tax';
4609 return 1 if $a->itemdesc eq 'Tax';
4610 return -1 if $b->itemdesc eq 'Other surcharges';
4611 return 1 if $a->itemdesc eq 'Other surcharges';
4612 $a->itemdesc cmp $b->itemdesc;
4617 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4618 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4621 sub _items_cust_bill_pkg {
4623 my $conf = $self->conf;
4624 my $cust_bill_pkgs = shift;
4627 my $format = $opt{format} || '';
4628 my $escape_function = $opt{escape_function} || sub { shift };
4629 my $format_function = $opt{format_function} || '';
4630 my $unsquelched = $opt{unsquelched} || '';
4631 my $section = $opt{section}->{description} if $opt{section};
4632 my $summary_page = $opt{summary_page} || '';
4633 my $multilocation = $opt{multilocation} || '';
4634 my $multisection = $opt{multisection} || '';
4635 my $discount_show_always = 0;
4637 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4640 my ($s, $r, $u) = ( undef, undef, undef );
4641 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4644 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4645 if ( $_ && !$cust_bill_pkg->hidden ) {
4646 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4647 $_->{amount} =~ s/^\-0\.00$/0.00/;
4648 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4650 if $_->{amount} != 0
4651 || $discount_show_always
4652 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4653 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4659 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4660 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4663 foreach my $display ( grep { defined($section)
4664 ? $_->section eq $section
4667 #grep { !$_->summary || !$summary_page } # bunk!
4668 grep { !$_->summary || $multisection }
4669 $cust_bill_pkg->cust_bill_pkg_display
4673 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4674 $display->billpkgdisplaynum. "\n"
4677 my $type = $display->type;
4679 my $desc = $cust_bill_pkg->desc;
4680 $desc = substr($desc, 0, $maxlength). '...'
4681 if $format eq 'latex' && length($desc) > $maxlength;
4683 my %details_opt = ( 'format' => $format,
4684 'escape_function' => $escape_function,
4685 'format_function' => $format_function,
4688 if ( $cust_bill_pkg->pkgnum > 0 ) {
4690 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4693 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4695 if ( (!$type || $type eq 'S')
4696 && ( $cust_bill_pkg->setup != 0
4697 || $cust_bill_pkg->setup_show_zero
4702 warn "$me _items_cust_bill_pkg adding setup\n"
4705 my $description = $desc;
4706 $description .= ' Setup'
4707 if $cust_bill_pkg->recur != 0
4708 || $discount_show_always
4709 || $cust_bill_pkg->recur_show_zero;
4712 unless ( $cust_pkg->part_pkg->hide_svc_detail
4713 || $cust_bill_pkg->hidden )
4716 push @d, map &{$escape_function}($_),
4717 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4718 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4720 if ( $multilocation ) {
4721 my $loc = $cust_pkg->location_label;
4722 $loc = substr($loc, 0, $maxlength). '...'
4723 if $format eq 'latex' && length($loc) > $maxlength;
4724 push @d, &{$escape_function}($loc);
4729 push @d, $cust_bill_pkg->details(%details_opt)
4730 if $cust_bill_pkg->recur == 0;
4732 if ( $cust_bill_pkg->hidden ) {
4733 $s->{amount} += $cust_bill_pkg->setup;
4734 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4735 push @{ $s->{ext_description} }, @d;
4739 description => $description,
4740 #pkgpart => $part_pkg->pkgpart,
4741 pkgnum => $cust_bill_pkg->pkgnum,
4742 amount => $cust_bill_pkg->setup,
4743 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4744 unit_amount => $cust_bill_pkg->unitsetup,
4745 quantity => $cust_bill_pkg->quantity,
4746 ext_description => \@d,
4752 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4754 $cust_bill_pkg->recur != 0
4755 || $cust_bill_pkg->setup == 0
4756 || $discount_show_always
4757 || $cust_bill_pkg->recur_show_zero
4762 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4765 my $is_summary = $display->summary;
4766 my $description = ($is_summary && $type && $type eq 'U')
4767 ? "Usage charges" : $desc;
4769 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4770 " - ". time2str($date_format, $cust_bill_pkg->edate).
4772 unless $conf->exists('disable_line_item_date_ranges')
4773 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4777 #at least until cust_bill_pkg has "past" ranges in addition to
4778 #the "future" sdate/edate ones... see #3032
4779 my @dates = ( $self->_date );
4780 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4781 push @dates, $prev->sdate if $prev;
4782 push @dates, undef if !$prev;
4784 unless ( $cust_pkg->part_pkg->hide_svc_detail
4785 || $cust_bill_pkg->itemdesc
4786 || $cust_bill_pkg->hidden
4787 || $is_summary && $type && $type eq 'U' )
4790 warn "$me _items_cust_bill_pkg adding service details\n"
4793 push @d, map &{$escape_function}($_),
4794 $cust_pkg->h_labels_short(@dates, 'I')
4795 #$cust_bill_pkg->edate,
4796 #$cust_bill_pkg->sdate)
4797 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4799 warn "$me _items_cust_bill_pkg done adding service details\n"
4802 if ( $multilocation ) {
4803 my $loc = $cust_pkg->location_label;
4804 $loc = substr($loc, 0, $maxlength). '...'
4805 if $format eq 'latex' && length($loc) > $maxlength;
4806 push @d, &{$escape_function}($loc);
4811 unless ( $is_summary ) {
4812 warn "$me _items_cust_bill_pkg adding details\n"
4815 #instead of omitting details entirely in this case (unwanted side
4816 # effects), just omit CDRs
4817 $details_opt{'format_function'} = sub { () }
4818 if $type && $type eq 'R';
4820 push @d, $cust_bill_pkg->details(%details_opt);
4823 warn "$me _items_cust_bill_pkg calculating amount\n"
4828 $amount = $cust_bill_pkg->recur;
4829 } elsif ($type eq 'R') {
4830 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4831 } elsif ($type eq 'U') {
4832 $amount = $cust_bill_pkg->usage;
4835 if ( !$type || $type eq 'R' ) {
4837 warn "$me _items_cust_bill_pkg adding recur\n"
4840 if ( $cust_bill_pkg->hidden ) {
4841 $r->{amount} += $amount;
4842 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4843 push @{ $r->{ext_description} }, @d;
4846 description => $description,
4847 #pkgpart => $part_pkg->pkgpart,
4848 pkgnum => $cust_bill_pkg->pkgnum,
4850 recur_show_zero => $cust_bill_pkg->recur_show_zero,
4851 unit_amount => $cust_bill_pkg->unitrecur,
4852 quantity => $cust_bill_pkg->quantity,
4853 ext_description => \@d,
4857 } else { # $type eq 'U'
4859 warn "$me _items_cust_bill_pkg adding usage\n"
4862 if ( $cust_bill_pkg->hidden ) {
4863 $u->{amount} += $amount;
4864 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4865 push @{ $u->{ext_description} }, @d;
4868 description => $description,
4869 #pkgpart => $part_pkg->pkgpart,
4870 pkgnum => $cust_bill_pkg->pkgnum,
4872 recur_show_zero => $cust_bill_pkg->recur_show_zero,
4873 unit_amount => $cust_bill_pkg->unitrecur,
4874 quantity => $cust_bill_pkg->quantity,
4875 ext_description => \@d,
4880 } # recurring or usage with recurring charge
4882 } else { #pkgnum tax or one-shot line item (??)
4884 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4887 if ( $cust_bill_pkg->setup != 0 ) {
4889 'description' => $desc,
4890 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4893 if ( $cust_bill_pkg->recur != 0 ) {
4895 'description' => "$desc (".
4896 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4897 time2str($date_format, $cust_bill_pkg->edate). ')',
4898 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4906 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4907 && $conf->exists('discount-show-always'));
4911 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4913 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4914 $_->{amount} =~ s/^\-0\.00$/0.00/;
4915 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4917 if $_->{amount} != 0
4918 || $discount_show_always
4919 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4920 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4924 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4931 sub _items_credits {
4932 my( $self, %opt ) = @_;
4933 my $trim_len = $opt{'trim_len'} || 60;
4937 foreach ( $self->cust_credited ) {
4939 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4941 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4942 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4943 $reason = " ($reason) " if $reason;
4946 #'description' => 'Credit ref\#'. $_->crednum.
4947 # " (". time2str("%x",$_->cust_credit->_date) .")".
4949 'description' => $self->mt('Credit applied').' '.
4950 time2str($date_format,$_->cust_credit->_date). $reason,
4951 'amount' => sprintf("%.2f",$_->amount),
4959 sub _items_payments {
4963 #get & print payments
4964 foreach ( $self->cust_bill_pay ) {
4966 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4969 'description' => $self->mt('Payment received').' '.
4970 time2str($date_format,$_->cust_pay->_date ),
4971 'amount' => sprintf("%.2f", $_->amount )
4979 =item call_details [ OPTION => VALUE ... ]
4981 Returns an array of CSV strings representing the call details for this invoice
4982 The only option available is the boolean prepend_billed_number
4987 my ($self, %opt) = @_;
4989 my $format_function = sub { shift };
4991 if ($opt{prepend_billed_number}) {
4992 $format_function = sub {
4996 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5001 my @details = map { $_->details( 'format_function' => $format_function,
5002 'escape_function' => sub{ return() },
5006 $self->cust_bill_pkg;
5007 my $header = $details[0];
5008 ( $header, grep { $_ ne $header } @details );
5018 =item process_reprint
5022 sub process_reprint {
5023 process_re_X('print', @_);
5026 =item process_reemail
5030 sub process_reemail {
5031 process_re_X('email', @_);
5039 process_re_X('fax', @_);
5047 process_re_X('ftp', @_);
5054 sub process_respool {
5055 process_re_X('spool', @_);
5058 use Storable qw(thaw);
5062 my( $method, $job ) = ( shift, shift );
5063 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5065 my $param = thaw(decode_base64(shift));
5066 warn Dumper($param) if $DEBUG;
5077 my($method, $job, %param ) = @_;
5079 warn "re_X $method for job $job with param:\n".
5080 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5083 #some false laziness w/search/cust_bill.html
5085 my $orderby = 'ORDER BY cust_bill._date';
5087 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5089 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5091 my @cust_bill = qsearch( {
5092 #'select' => "cust_bill.*",
5093 'table' => 'cust_bill',
5094 'addl_from' => $addl_from,
5096 'extra_sql' => $extra_sql,
5097 'order_by' => $orderby,
5101 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5103 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5106 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5107 foreach my $cust_bill ( @cust_bill ) {
5108 $cust_bill->$method();
5110 if ( $job ) { #progressbar foo
5112 if ( time - $min_sec > $last ) {
5113 my $error = $job->update_statustext(
5114 int( 100 * $num / scalar(@cust_bill) )
5116 die $error if $error;
5127 =head1 CLASS METHODS
5133 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5138 my ($class, $start, $end) = @_;
5140 $class->paid_sql($start, $end). ' - '.
5141 $class->credited_sql($start, $end);
5146 Returns an SQL fragment to retreive the net amount (charged minus credited).
5151 my ($class, $start, $end) = @_;
5152 'charged - '. $class->credited_sql($start, $end);
5157 Returns an SQL fragment to retreive the amount paid against this invoice.
5162 my ($class, $start, $end) = @_;
5163 $start &&= "AND cust_bill_pay._date <= $start";
5164 $end &&= "AND cust_bill_pay._date > $end";
5165 $start = '' unless defined($start);
5166 $end = '' unless defined($end);
5167 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5168 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5173 Returns an SQL fragment to retreive the amount credited against this invoice.
5178 my ($class, $start, $end) = @_;
5179 $start &&= "AND cust_credit_bill._date <= $start";
5180 $end &&= "AND cust_credit_bill._date > $end";
5181 $start = '' unless defined($start);
5182 $end = '' unless defined($end);
5183 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5184 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5189 Returns an SQL fragment to retrieve the due date of an invoice.
5190 Currently only supported on PostgreSQL.
5195 my $conf = new FS::Conf;
5199 cust_bill.invoice_terms,
5200 cust_main.invoice_terms,
5201 \''.($conf->config('invoice_default_terms') || '').'\'
5202 ), E\'Net (\\\\d+)\'
5204 ) * 86400 + cust_bill._date'
5207 =item search_sql_where HASHREF
5209 Class method which returns an SQL WHERE fragment to search for parameters
5210 specified in HASHREF. Valid parameters are
5216 List reference of start date, end date, as UNIX timestamps.
5226 List reference of charged limits (exclusive).
5230 List reference of charged limits (exclusive).
5234 flag, return open invoices only
5238 flag, return net invoices only
5242 =item newest_percust
5246 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5250 sub search_sql_where {
5251 my($class, $param) = @_;
5253 warn "$me search_sql_where called with params: \n".
5254 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5260 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5261 push @search, "cust_main.agentnum = $1";
5265 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5266 push @search, "cust_bill.custnum = $1";
5270 if ( $param->{_date} ) {
5271 my($beginning, $ending) = @{$param->{_date}};
5273 push @search, "cust_bill._date >= $beginning",
5274 "cust_bill._date < $ending";
5278 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5279 push @search, "cust_bill.invnum >= $1";
5281 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5282 push @search, "cust_bill.invnum <= $1";
5286 if ( $param->{charged} ) {
5287 my @charged = ref($param->{charged})
5288 ? @{ $param->{charged} }
5289 : ($param->{charged});
5291 push @search, map { s/^charged/cust_bill.charged/; $_; }
5295 my $owed_sql = FS::cust_bill->owed_sql;
5298 if ( $param->{owed} ) {
5299 my @owed = ref($param->{owed})
5300 ? @{ $param->{owed} }
5302 push @search, map { s/^owed/$owed_sql/; $_; }
5307 push @search, "0 != $owed_sql"
5308 if $param->{'open'};
5309 push @search, '0 != '. FS::cust_bill->net_sql
5313 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5314 if $param->{'days'};
5317 if ( $param->{'newest_percust'} ) {
5319 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5320 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5322 my @newest_where = map { my $x = $_;
5323 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5326 grep ! /^cust_main./, @search;
5327 my $newest_where = scalar(@newest_where)
5328 ? ' AND '. join(' AND ', @newest_where)
5332 push @search, "cust_bill._date = (
5333 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5334 WHERE newest_cust_bill.custnum = cust_bill.custnum
5340 #agent virtualization
5341 my $curuser = $FS::CurrentUser::CurrentUser;
5342 if ( $curuser->username eq 'fs_queue'
5343 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5345 my $newuser = qsearchs('access_user', {
5346 'username' => $username,
5350 $curuser = $newuser;
5352 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5355 push @search, $curuser->agentnums_sql;
5357 join(' AND ', @search );
5369 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5370 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base