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 sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
147 =item promised_date - customer promised payment date, for collection
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
165 sub cust_linked { $_[0]->cust_main_custnum; }
166 sub cust_unlinked_msg {
168 "WARNING: can't find cust_main.custnum ". $self->custnum.
169 ' (cust_bill.invnum '. $self->invnum. ')';
174 Adds this invoice to the database ("Posts" the invoice). If there is an error,
175 returns the error, otherwise returns false.
181 warn "$me insert called\n" if $DEBUG;
183 local $SIG{HUP} = 'IGNORE';
184 local $SIG{INT} = 'IGNORE';
185 local $SIG{QUIT} = 'IGNORE';
186 local $SIG{TERM} = 'IGNORE';
187 local $SIG{TSTP} = 'IGNORE';
188 local $SIG{PIPE} = 'IGNORE';
190 my $oldAutoCommit = $FS::UID::AutoCommit;
191 local $FS::UID::AutoCommit = 0;
194 my $error = $self->SUPER::insert;
196 $dbh->rollback if $oldAutoCommit;
200 if ( $self->get('cust_bill_pkg') ) {
201 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202 $cust_bill_pkg->invnum($self->invnum);
203 my $error = $cust_bill_pkg->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "can't create invoice line item: $error";
211 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 This method now works but you probably shouldn't use it. Instead, apply a
219 credit against the invoice.
221 Using this method to delete invoices outright is really, really bad. There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
225 Really, don't use it.
231 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
256 foreach my $linked ( $self->$table() ) {
257 my $error = $linked->delete;
259 $dbh->rollback if $oldAutoCommit;
266 my $error = $self->SUPER::delete(@_);
268 $dbh->rollback if $oldAutoCommit;
272 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 =item replace [ OLD_RECORD ]
280 You can, but probably shouldn't modify invoices...
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record. If there is an error, returns the error,
284 otherwise returns false.
288 #replace can be inherited from Record.pm
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
294 my( $new, $old ) = ( shift, shift );
295 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296 #return "Can't change _date!" unless $old->_date eq $new->_date;
297 return "Can't change _date" unless $old->_date == $new->_date;
298 return "Can't change charged" unless $old->charged == $new->charged
299 || $old->charged == 0
300 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
306 =item add_cc_surcharge
312 sub add_cc_surcharge {
313 my ($self, $pkgnum, $amount) = (shift, shift, shift);
316 my $cust_bill_pkg = new FS::cust_bill_pkg({
317 'invnum' => $self->invnum,
321 $error = $cust_bill_pkg->insert;
322 return $error if $error;
324 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325 $self->charged($self->charged+$amount);
326 $error = $self->replace;
327 return $error if $error;
329 $self->apply_payments_and_credits;
335 Checks all fields to make sure this is a valid invoice. If there is an error,
336 returns the error, otherwise returns false. Called by the insert and replace
345 $self->ut_numbern('invnum')
346 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347 || $self->ut_numbern('_date')
348 || $self->ut_money('charged')
349 || $self->ut_numbern('printed')
350 || $self->ut_enum('closed', [ '', 'Y' ])
351 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352 || $self->ut_numbern('agent_invid') #varchar?
354 return $error if $error;
356 $self->_date(time) unless $self->_date;
358 $self->printed(0) if $self->printed eq '';
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
372 my $conf = $self->conf;
373 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374 return $self->agent_invid;
376 return $self->invnum;
382 Returns a list consisting of the total previous balance for this customer,
383 followed by the previous outstanding invoices (as FS::cust_bill objects also).
390 my @cust_bill = sort { $a->_date <=> $b->_date }
391 grep { $_->owed != 0 }
392 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
393 '_date' => { op=>'<', value=>$self->_date },
396 foreach ( @cust_bill ) { $total += $_->owed; }
402 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
409 { 'table' => 'cust_bill_pkg',
410 'hashref' => { 'invnum' => $self->invnum },
411 'order_by' => 'ORDER BY billpkgnum',
416 =item cust_bill_pkg_pkgnum PKGNUM
418 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
423 sub cust_bill_pkg_pkgnum {
424 my( $self, $pkgnum ) = @_;
426 { 'table' => 'cust_bill_pkg',
427 'hashref' => { 'invnum' => $self->invnum,
430 'order_by' => 'ORDER BY billpkgnum',
437 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
444 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
445 $self->cust_bill_pkg;
447 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
452 Returns true if any of the packages (or their definitions) corresponding to the
453 line items for this invoice have the no_auto flag set.
459 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
462 =item open_cust_bill_pkg
464 Returns the open line items for this invoice.
466 Note that cust_bill_pkg with both setup and recur fees are returned as two
467 separate line items, each with only one fee.
471 # modeled after cust_main::open_cust_bill
472 sub open_cust_bill_pkg {
475 # grep { $_->owed > 0 } $self->cust_bill_pkg
477 my %other = ( 'recur' => 'setup',
478 'setup' => 'recur', );
480 foreach my $field ( qw( recur setup )) {
481 push @open, map { $_->set( $other{$field}, 0 ); $_; }
482 grep { $_->owed($field) > 0 }
483 $self->cust_bill_pkg;
489 =item cust_bill_event
491 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
495 sub cust_bill_event {
497 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
500 =item num_cust_bill_event
502 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
506 sub num_cust_bill_event {
509 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
510 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
511 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
512 $sth->fetchrow_arrayref->[0];
517 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
521 #false laziness w/cust_pkg.pm
525 'table' => 'cust_event',
526 'addl_from' => 'JOIN part_event USING ( eventpart )',
527 'hashref' => { 'tablenum' => $self->invnum },
528 'extra_sql' => " AND eventtable = 'cust_bill' ",
534 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
538 #false laziness w/cust_pkg.pm
542 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
543 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
544 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
545 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
546 $sth->fetchrow_arrayref->[0];
551 Returns the customer (see L<FS::cust_main>) for this invoice.
557 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
560 =item cust_suspend_if_balance_over AMOUNT
562 Suspends the customer associated with this invoice if the total amount owed on
563 this invoice and all older invoices is greater than the specified amount.
565 Returns a list: an empty list on success or a list of errors.
569 sub cust_suspend_if_balance_over {
570 my( $self, $amount ) = ( shift, shift );
571 my $cust_main = $self->cust_main;
572 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
575 $cust_main->suspend(@_);
581 Depreciated. See the cust_credited method.
583 #Returns a list consisting of the total previous credited (see
584 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
585 #outstanding credits (FS::cust_credit objects).
591 croak "FS::cust_bill->cust_credit depreciated; see ".
592 "FS::cust_bill->cust_credit_bill";
595 #my @cust_credit = sort { $a->_date <=> $b->_date }
596 # grep { $_->credited != 0 && $_->_date < $self->_date }
597 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
599 #foreach (@cust_credit) { $total += $_->credited; }
600 #$total, @cust_credit;
605 Depreciated. See the cust_bill_pay method.
607 #Returns all payments (see L<FS::cust_pay>) for this invoice.
613 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
615 #sort { $a->_date <=> $b->_date }
616 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
622 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
625 sub cust_bill_pay_batch {
627 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
632 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
638 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
639 sort { $a->_date <=> $b->_date }
640 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
645 =item cust_credit_bill
647 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
653 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
654 sort { $a->_date <=> $b->_date }
655 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
659 sub cust_credit_bill {
660 shift->cust_credited(@_);
663 #=item cust_bill_pay_pkgnum PKGNUM
665 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
666 #with matching pkgnum.
670 #sub cust_bill_pay_pkgnum {
671 # my( $self, $pkgnum ) = @_;
672 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
673 # sort { $a->_date <=> $b->_date }
674 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
675 # 'pkgnum' => $pkgnum,
680 =item cust_bill_pay_pkg PKGNUM
682 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
683 applied against the matching pkgnum.
687 sub cust_bill_pay_pkg {
688 my( $self, $pkgnum ) = @_;
691 'select' => 'cust_bill_pay_pkg.*',
692 'table' => 'cust_bill_pay_pkg',
693 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
694 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
695 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
696 " AND cust_bill_pkg.pkgnum = $pkgnum",
701 #=item cust_credited_pkgnum PKGNUM
703 #=item cust_credit_bill_pkgnum PKGNUM
705 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
706 #with matching pkgnum.
710 #sub cust_credited_pkgnum {
711 # my( $self, $pkgnum ) = @_;
712 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
713 # sort { $a->_date <=> $b->_date }
714 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
715 # 'pkgnum' => $pkgnum,
720 #sub cust_credit_bill_pkgnum {
721 # shift->cust_credited_pkgnum(@_);
724 =item cust_credit_bill_pkg PKGNUM
726 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
727 applied against the matching pkgnum.
731 sub cust_credit_bill_pkg {
732 my( $self, $pkgnum ) = @_;
735 'select' => 'cust_credit_bill_pkg.*',
736 'table' => 'cust_credit_bill_pkg',
737 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
738 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
739 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
740 " AND cust_bill_pkg.pkgnum = $pkgnum",
745 =item cust_bill_batch
747 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
751 sub cust_bill_batch {
753 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
758 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
759 hash keyed by term length.
765 FS::discount_plan->all($self);
770 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
777 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
779 foreach (@taxlines) { $total += $_->setup; }
785 Returns the amount owed (still outstanding) on this invoice, which is charged
786 minus all payment applications (see L<FS::cust_bill_pay>) and credit
787 applications (see L<FS::cust_credit_bill>).
793 my $balance = $self->charged;
794 $balance -= $_->amount foreach ( $self->cust_bill_pay );
795 $balance -= $_->amount foreach ( $self->cust_credited );
796 $balance = sprintf( "%.2f", $balance);
797 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
802 my( $self, $pkgnum ) = @_;
804 #my $balance = $self->charged;
806 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
808 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
809 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
811 $balance = sprintf( "%.2f", $balance);
812 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
818 Returns true if this invoice should be hidden. See the
819 selfservice-hide_invoices-taxclass configuraiton setting.
825 my $conf = $self->conf;
826 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
828 my @cust_bill_pkg = $self->cust_bill_pkg;
829 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
830 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
833 =item apply_payments_and_credits [ OPTION => VALUE ... ]
835 Applies unapplied payments and credits to this invoice.
837 A hash of optional arguments may be passed. Currently "manual" is supported.
838 If true, a payment receipt is sent instead of a statement when
839 'payment_receipt_email' configuration option is set.
841 If there is an error, returns the error, otherwise returns false.
845 sub apply_payments_and_credits {
846 my( $self, %options ) = @_;
847 my $conf = $self->conf;
849 local $SIG{HUP} = 'IGNORE';
850 local $SIG{INT} = 'IGNORE';
851 local $SIG{QUIT} = 'IGNORE';
852 local $SIG{TERM} = 'IGNORE';
853 local $SIG{TSTP} = 'IGNORE';
854 local $SIG{PIPE} = 'IGNORE';
856 my $oldAutoCommit = $FS::UID::AutoCommit;
857 local $FS::UID::AutoCommit = 0;
860 $self->select_for_update; #mutex
862 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
863 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
865 if ( $conf->exists('pkg-balances') ) {
866 # limit @payments & @credits to those w/ a pkgnum grepped from $self
867 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
868 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
869 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
872 while ( $self->owed > 0 and ( @payments || @credits ) ) {
875 if ( @payments && @credits ) {
877 #decide which goes first by weight of top (unapplied) line item
879 my @open_lineitems = $self->open_cust_bill_pkg;
882 max( map { $_->part_pkg->pay_weight || 0 }
887 my $max_credit_weight =
888 max( map { $_->part_pkg->credit_weight || 0 }
894 #if both are the same... payments first? it has to be something
895 if ( $max_pay_weight >= $max_credit_weight ) {
901 } elsif ( @payments ) {
903 } elsif ( @credits ) {
906 die "guru meditation #12 and 35";
910 if ( $app eq 'pay' ) {
912 my $payment = shift @payments;
913 $unapp_amount = $payment->unapplied;
914 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
915 $app->pkgnum( $payment->pkgnum )
916 if $conf->exists('pkg-balances') && $payment->pkgnum;
918 } elsif ( $app eq 'credit' ) {
920 my $credit = shift @credits;
921 $unapp_amount = $credit->credited;
922 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
923 $app->pkgnum( $credit->pkgnum )
924 if $conf->exists('pkg-balances') && $credit->pkgnum;
927 die "guru meditation #12 and 35";
931 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
932 warn "owed_pkgnum ". $app->pkgnum;
933 $owed = $self->owed_pkgnum($app->pkgnum);
937 next unless $owed > 0;
939 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
940 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
942 $app->invnum( $self->invnum );
944 my $error = $app->insert(%options);
946 $dbh->rollback if $oldAutoCommit;
947 return "Error inserting ". $app->table. " record: $error";
949 die $error if $error;
953 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
958 =item generate_email OPTION => VALUE ...
966 sender address, required
970 alternate template name, optional
974 text attachment arrayref, optional
978 email subject, optional
982 notice name instead of "Invoice", optional
986 Returns an argument list to be passed to L<FS::Misc::send_email>.
996 my $conf = $self->conf;
998 my $me = '[FS::cust_bill::generate_email]';
1001 'from' => $args{'from'},
1002 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1006 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1007 'template' => $args{'template'},
1008 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1009 'no_coupon' => $args{'no_coupon'},
1012 my $cust_main = $self->cust_main;
1014 if (ref($args{'to'}) eq 'ARRAY') {
1015 $return{'to'} = $args{'to'};
1017 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1018 $cust_main->invoicing_list
1022 if ( $conf->exists('invoice_html') ) {
1024 warn "$me creating HTML/text multipart message"
1027 $return{'nobody'} = 1;
1029 my $alternative = build MIME::Entity
1030 'Type' => 'multipart/alternative',
1031 #'Encoding' => '7bit',
1032 'Disposition' => 'inline'
1036 if ( $conf->exists('invoice_email_pdf')
1037 and scalar($conf->config('invoice_email_pdf_note')) ) {
1039 warn "$me using 'invoice_email_pdf_note' in multipart message"
1041 $data = [ map { $_ . "\n" }
1042 $conf->config('invoice_email_pdf_note')
1047 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1049 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1050 $data = $args{'print_text'};
1052 $data = [ $self->print_text(\%opt) ];
1057 $alternative->attach(
1058 'Type' => 'text/plain',
1059 'Encoding' => 'quoted-printable',
1060 #'Encoding' => '7bit',
1062 'Disposition' => 'inline',
1069 if ( $conf->exists('invoice_email_pdf')
1070 and scalar($conf->config('invoice_email_pdf_note')) ) {
1072 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1076 $args{'from'} =~ /\@([\w\.\-]+)/;
1077 my $from = $1 || 'example.com';
1078 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1081 my $agentnum = $cust_main->agentnum;
1082 if ( defined($args{'template'}) && length($args{'template'})
1083 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1086 $logo = 'logo_'. $args{'template'}. '.png';
1090 my $image_data = $conf->config_binary( $logo, $agentnum);
1092 $image = build MIME::Entity
1093 'Type' => 'image/png',
1094 'Encoding' => 'base64',
1095 'Data' => $image_data,
1096 'Filename' => 'logo.png',
1097 'Content-ID' => "<$content_id>",
1100 if ($conf->exists('invoice-barcode')) {
1101 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1102 $barcode = build MIME::Entity
1103 'Type' => 'image/png',
1104 'Encoding' => 'base64',
1105 'Data' => $self->invoice_barcode(0),
1106 'Filename' => 'barcode.png',
1107 'Content-ID' => "<$barcode_content_id>",
1109 $opt{'barcode_cid'} = $barcode_content_id;
1112 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1115 $alternative->attach(
1116 'Type' => 'text/html',
1117 'Encoding' => 'quoted-printable',
1118 'Data' => [ '<html>',
1121 ' '. encode_entities($return{'subject'}),
1124 ' <body bgcolor="#e8e8e8">',
1129 'Disposition' => 'inline',
1130 #'Filename' => 'invoice.pdf',
1134 my @otherparts = ();
1135 if ( $cust_main->email_csv_cdr ) {
1137 push @otherparts, build MIME::Entity
1138 'Type' => 'text/csv',
1139 'Encoding' => '7bit',
1140 'Data' => [ map { "$_\n" }
1141 $self->call_details('prepend_billed_number' => 1)
1143 'Disposition' => 'attachment',
1144 'Filename' => 'usage-'. $self->invnum. '.csv',
1149 if ( $conf->exists('invoice_email_pdf') ) {
1154 # multipart/alternative
1160 my $related = build MIME::Entity 'Type' => 'multipart/related',
1161 'Encoding' => '7bit';
1163 #false laziness w/Misc::send_email
1164 $related->head->replace('Content-type',
1165 $related->mime_type.
1166 '; boundary="'. $related->head->multipart_boundary. '"'.
1167 '; type=multipart/alternative'
1170 $related->add_part($alternative);
1172 $related->add_part($image) if $image;
1174 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1176 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1180 #no other attachment:
1182 # multipart/alternative
1187 $return{'content-type'} = 'multipart/related';
1188 if ($conf->exists('invoice-barcode') && $barcode) {
1189 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1191 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1193 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1194 #$return{'disposition'} = 'inline';
1200 if ( $conf->exists('invoice_email_pdf') ) {
1201 warn "$me creating PDF attachment"
1204 #mime parts arguments a la MIME::Entity->build().
1205 $return{'mimeparts'} = [
1206 { $self->mimebuild_pdf(\%opt) }
1210 if ( $conf->exists('invoice_email_pdf')
1211 and scalar($conf->config('invoice_email_pdf_note')) ) {
1213 warn "$me using 'invoice_email_pdf_note'"
1215 $return{'body'} = [ map { $_ . "\n" }
1216 $conf->config('invoice_email_pdf_note')
1221 warn "$me not using 'invoice_email_pdf_note'"
1223 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1224 $return{'body'} = $args{'print_text'};
1226 $return{'body'} = [ $self->print_text(\%opt) ];
1239 Returns a list suitable for passing to MIME::Entity->build(), representing
1240 this invoice as PDF attachment.
1247 'Type' => 'application/pdf',
1248 'Encoding' => 'base64',
1249 'Data' => [ $self->print_pdf(@_) ],
1250 'Disposition' => 'attachment',
1251 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1255 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1257 Sends this invoice to the destinations configured for this customer: sends
1258 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1260 Options can be passed as a hashref (recommended) or as a list of up to
1261 four values for templatename, agentnum, invoice_from and amount.
1263 I<template>, if specified, is the name of a suffix for alternate invoices.
1265 I<agentnum>, if specified, means that this invoice will only be sent for customers
1266 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1267 single agent) or an arrayref of agentnums.
1269 I<invoice_from>, if specified, overrides the default email invoice From: address.
1271 I<amount>, if specified, only sends the invoice if the total amount owed on this
1272 invoice and all older invoices is greater than the specified amount.
1274 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1278 sub queueable_send {
1281 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1282 or die "invalid invoice number: " . $opt{invnum};
1284 my @args = ( $opt{template}, $opt{agentnum} );
1285 push @args, $opt{invoice_from}
1286 if exists($opt{invoice_from}) && $opt{invoice_from};
1288 my $error = $self->send( @args );
1289 die $error if $error;
1295 my $conf = $self->conf;
1297 my( $template, $invoice_from, $notice_name );
1299 my $balance_over = 0;
1303 $template = $opt->{'template'} || '';
1304 if ( $agentnums = $opt->{'agentnum'} ) {
1305 $agentnums = [ $agentnums ] unless ref($agentnums);
1307 $invoice_from = $opt->{'invoice_from'};
1308 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1309 $notice_name = $opt->{'notice_name'};
1311 $template = scalar(@_) ? shift : '';
1312 if ( scalar(@_) && $_[0] ) {
1313 $agentnums = ref($_[0]) ? shift : [ shift ];
1315 $invoice_from = shift if scalar(@_);
1316 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1319 my $cust_main = $self->cust_main;
1321 return 'N/A' unless ! $agentnums
1322 or grep { $_ == $cust_main->agentnum } @$agentnums;
1325 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1327 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1328 $conf->config('invoice_from', $cust_main->agentnum );
1331 'template' => $template,
1332 'invoice_from' => $invoice_from,
1333 'notice_name' => ( $notice_name || 'Invoice' ),
1336 my @invoicing_list = $cust_main->invoicing_list;
1338 #$self->email_invoice(\%opt)
1340 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1341 && ! $self->invoice_noemail;
1343 #$self->print_invoice(\%opt)
1345 if grep { $_ eq 'POST' } @invoicing_list; #postal
1347 $self->fax_invoice(\%opt)
1348 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1354 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1356 Emails this invoice.
1358 Options can be passed as a hashref (recommended) or as a list of up to
1359 two values for templatename and invoice_from.
1361 I<template>, if specified, is the name of a suffix for alternate invoices.
1363 I<invoice_from>, if specified, overrides the default email invoice From: address.
1365 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1369 sub queueable_email {
1372 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1373 or die "invalid invoice number: " . $opt{invnum};
1375 my %args = ( 'template' => $opt{template} );
1376 $args{$_} = $opt{$_}
1377 foreach grep { exists($opt{$_}) && $opt{$_} }
1378 qw( invoice_from notice_name no_coupon );
1380 my $error = $self->email( \%args );
1381 die $error if $error;
1385 #sub email_invoice {
1388 return if $self->hide;
1389 my $conf = $self->conf;
1391 my( $template, $invoice_from, $notice_name, $no_coupon );
1394 $template = $opt->{'template'} || '';
1395 $invoice_from = $opt->{'invoice_from'};
1396 $notice_name = $opt->{'notice_name'} || 'Invoice';
1397 $no_coupon = $opt->{'no_coupon'} || 0;
1399 $template = scalar(@_) ? shift : '';
1400 $invoice_from = shift if scalar(@_);
1401 $notice_name = 'Invoice';
1405 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1406 $conf->config('invoice_from', $self->cust_main->agentnum );
1408 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1409 $self->cust_main->invoicing_list;
1411 if ( ! @invoicing_list ) { #no recipients
1412 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1413 die 'No recipients for customer #'. $self->custnum;
1415 #default: better to notify this person than silence
1416 @invoicing_list = ($invoice_from);
1420 my $subject = $self->email_subject($template);
1422 my $error = send_email(
1423 $self->generate_email(
1424 'from' => $invoice_from,
1425 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1426 'subject' => $subject,
1427 'template' => $template,
1428 'notice_name' => $notice_name,
1429 'no_coupon' => $no_coupon,
1432 die "can't email invoice: $error\n" if $error;
1433 #die "$error\n" if $error;
1439 my $conf = $self->conf;
1441 #my $template = scalar(@_) ? shift : '';
1444 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1447 my $cust_main = $self->cust_main;
1448 my $name = $cust_main->name;
1449 my $name_short = $cust_main->name_short;
1450 my $invoice_number = $self->invnum;
1451 my $invoice_date = $self->_date_pretty;
1453 eval qq("$subject");
1456 =item lpr_data HASHREF | [ TEMPLATE ]
1458 Returns the postscript or plaintext for this invoice as an arrayref.
1460 Options can be passed as a hashref (recommended) or as a single optional value
1463 I<template>, if specified, is the name of a suffix for alternate invoices.
1465 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1471 my $conf = $self->conf;
1472 my( $template, $notice_name );
1475 $template = $opt->{'template'} || '';
1476 $notice_name = $opt->{'notice_name'} || 'Invoice';
1478 $template = scalar(@_) ? shift : '';
1479 $notice_name = 'Invoice';
1483 'template' => $template,
1484 'notice_name' => $notice_name,
1487 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1488 [ $self->$method( \%opt ) ];
1491 =item print HASHREF | [ TEMPLATE ]
1493 Prints this invoice.
1495 Options can be passed as a hashref (recommended) or as a single optional
1498 I<template>, if specified, is the name of a suffix for alternate invoices.
1500 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1504 #sub print_invoice {
1507 return if $self->hide;
1508 my $conf = $self->conf;
1510 my( $template, $notice_name );
1513 $template = $opt->{'template'} || '';
1514 $notice_name = $opt->{'notice_name'} || 'Invoice';
1516 $template = scalar(@_) ? shift : '';
1517 $notice_name = 'Invoice';
1521 'template' => $template,
1522 'notice_name' => $notice_name,
1525 if($conf->exists('invoice_print_pdf')) {
1526 # Add the invoice to the current batch.
1527 $self->batch_invoice(\%opt);
1530 do_print $self->lpr_data(\%opt);
1534 =item fax_invoice HASHREF | [ TEMPLATE ]
1538 Options can be passed as a hashref (recommended) or as a single optional
1541 I<template>, if specified, is the name of a suffix for alternate invoices.
1543 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1549 return if $self->hide;
1550 my $conf = $self->conf;
1552 my( $template, $notice_name );
1555 $template = $opt->{'template'} || '';
1556 $notice_name = $opt->{'notice_name'} || 'Invoice';
1558 $template = scalar(@_) ? shift : '';
1559 $notice_name = 'Invoice';
1562 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1563 unless $conf->exists('invoice_latex');
1565 my $dialstring = $self->cust_main->getfield('fax');
1569 'template' => $template,
1570 'notice_name' => $notice_name,
1573 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1574 'dialstring' => $dialstring,
1576 die $error if $error;
1580 =item batch_invoice [ HASHREF ]
1582 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1583 isn't an open batch, one will be created.
1588 my ($self, $opt) = @_;
1589 my $bill_batch = $self->get_open_bill_batch;
1590 my $cust_bill_batch = FS::cust_bill_batch->new({
1591 batchnum => $bill_batch->batchnum,
1592 invnum => $self->invnum,
1594 return $cust_bill_batch->insert($opt);
1597 =item get_open_batch
1599 Returns the currently open batch as an FS::bill_batch object, creating a new
1600 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1605 sub get_open_bill_batch {
1607 my $conf = $self->conf;
1608 my $hashref = { status => 'O' };
1609 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1610 ? $self->cust_main->agentnum
1612 my $batch = qsearchs('bill_batch', $hashref);
1613 return $batch if $batch;
1614 $batch = FS::bill_batch->new($hashref);
1615 my $error = $batch->insert;
1616 die $error if $error;
1620 =item ftp_invoice [ TEMPLATENAME ]
1622 Sends this invoice data via FTP.
1624 TEMPLATENAME is unused?
1630 my $conf = $self->conf;
1631 my $template = scalar(@_) ? shift : '';
1634 'protocol' => 'ftp',
1635 'server' => $conf->config('cust_bill-ftpserver'),
1636 'username' => $conf->config('cust_bill-ftpusername'),
1637 'password' => $conf->config('cust_bill-ftppassword'),
1638 'dir' => $conf->config('cust_bill-ftpdir'),
1639 'format' => $conf->config('cust_bill-ftpformat'),
1643 =item spool_invoice [ TEMPLATENAME ]
1645 Spools this invoice data (see L<FS::spool_csv>)
1647 TEMPLATENAME is unused?
1653 my $conf = $self->conf;
1654 my $template = scalar(@_) ? shift : '';
1657 'format' => $conf->config('cust_bill-spoolformat'),
1658 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1662 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1664 Like B<send>, but only sends the invoice if it is the newest open invoice for
1669 sub send_if_newest {
1674 grep { $_->owed > 0 }
1675 qsearch('cust_bill', {
1676 'custnum' => $self->custnum,
1677 #'_date' => { op=>'>', value=>$self->_date },
1678 'invnum' => { op=>'>', value=>$self->invnum },
1685 =item send_csv OPTION => VALUE, ...
1687 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1691 protocol - currently only "ftp"
1697 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1698 and YYMMDDHHMMSS is a timestamp.
1700 See L</print_csv> for a description of the output format.
1705 my($self, %opt) = @_;
1709 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1710 mkdir $spooldir, 0700 unless -d $spooldir;
1712 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1713 my $file = "$spooldir/$tracctnum.csv";
1715 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1717 open(CSV, ">$file") or die "can't open $file: $!";
1725 if ( $opt{protocol} eq 'ftp' ) {
1726 eval "use Net::FTP;";
1728 $net = Net::FTP->new($opt{server}) or die @$;
1730 die "unknown protocol: $opt{protocol}";
1733 $net->login( $opt{username}, $opt{password} )
1734 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1736 $net->binary or die "can't set binary mode";
1738 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1740 $net->put($file) or die "can't put $file: $!";
1750 Spools CSV invoice data.
1756 =item format - 'default' or 'billco'
1758 =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>).
1760 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1762 =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.
1769 my($self, %opt) = @_;
1771 my $cust_main = $self->cust_main;
1773 if ( $opt{'dest'} ) {
1774 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1775 $cust_main->invoicing_list;
1776 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1777 || ! keys %invoicing_list;
1780 if ( $opt{'balanceover'} ) {
1782 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1785 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1786 mkdir $spooldir, 0700 unless -d $spooldir;
1788 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1792 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1793 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1796 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1798 open(CSV, ">>$file") or die "can't open $file: $!";
1799 flock(CSV, LOCK_EX);
1804 if ( lc($opt{'format'}) eq 'billco' ) {
1806 flock(CSV, LOCK_UN);
1811 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1814 open(CSV,">>$file") or die "can't open $file: $!";
1815 flock(CSV, LOCK_EX);
1821 flock(CSV, LOCK_UN);
1828 =item print_csv OPTION => VALUE, ...
1830 Returns CSV data for this invoice.
1834 format - 'default' or 'billco'
1836 Returns a list consisting of two scalars. The first is a single line of CSV
1837 header information for this invoice. The second is one or more lines of CSV
1838 detail information for this invoice.
1840 If I<format> is not specified or "default", the fields of the CSV file are as
1843 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1847 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1849 B<record_type> is C<cust_bill> for the initial header line only. The
1850 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1851 fields are filled in.
1853 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1854 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1857 =item invnum - invoice number
1859 =item custnum - customer number
1861 =item _date - invoice date
1863 =item charged - total invoice amount
1865 =item first - customer first name
1867 =item last - customer first name
1869 =item company - company name
1871 =item address1 - address line 1
1873 =item address2 - address line 1
1883 =item pkg - line item description
1885 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1887 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1889 =item sdate - start date for recurring fee
1891 =item edate - end date for recurring fee
1895 If I<format> is "billco", the fields of the header CSV file are as follows:
1897 +-------------------------------------------------------------------+
1898 | FORMAT HEADER FILE |
1899 |-------------------------------------------------------------------|
1900 | Field | Description | Name | Type | Width |
1901 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1902 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1903 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1904 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1905 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1906 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1907 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1908 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1909 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1910 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1911 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1912 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1913 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1914 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1915 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1916 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1917 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1918 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1919 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1920 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1921 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1922 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1923 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1924 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1925 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1926 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1927 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1928 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1929 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1930 +-------+-------------------------------+------------+------+-------+
1932 If I<format> is "billco", the fields of the detail CSV file are as follows:
1934 FORMAT FOR DETAIL FILE
1936 Field | Description | Name | Type | Width
1937 1 | N/A-Leave Empty | RC | CHAR | 2
1938 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1939 3 | Account Number | TRACCTNUM | CHAR | 15
1940 4 | Invoice Number | TRINVOICE | CHAR | 15
1941 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1942 6 | Transaction Detail | DETAILS | CHAR | 100
1943 7 | Amount | AMT | NUM* | 9
1944 8 | Line Format Control** | LNCTRL | CHAR | 2
1945 9 | Grouping Code | GROUP | CHAR | 2
1946 10 | User Defined | ACCT CODE | CHAR | 15
1951 my($self, %opt) = @_;
1953 eval "use Text::CSV_XS";
1956 my $cust_main = $self->cust_main;
1958 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1960 if ( lc($opt{'format'}) eq 'billco' ) {
1963 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1965 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1967 my( $previous_balance, @unused ) = $self->previous; #previous balance
1969 my $pmt_cr_applied = 0;
1970 $pmt_cr_applied += $_->{'amount'}
1971 foreach ( $self->_items_payments, $self->_items_credits ) ;
1973 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1976 '', # 1 | N/A-Leave Empty CHAR 2
1977 '', # 2 | N/A-Leave Empty CHAR 15
1978 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1979 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1980 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1981 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1982 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1983 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1984 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1985 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1986 '', # 10 | Ancillary Billing Information CHAR 30
1987 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1988 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1991 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1994 $duedate, # 14 | Bill Due Date CHAR 10
1996 $previous_balance, # 15 | Previous Balance NUM* 9
1997 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1998 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1999 $totaldue, # 18 | Total Amt Due NUM* 9
2000 $totaldue, # 19 | Total Amt Due NUM* 9
2001 '', # 20 | 30 Day Aging NUM* 9
2002 '', # 21 | 60 Day Aging NUM* 9
2003 '', # 22 | 90 Day Aging NUM* 9
2004 'N', # 23 | Y/N CHAR 1
2005 '', # 24 | Remittance automation CHAR 100
2006 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2007 $self->custnum, # 26 | Customer Reference Number CHAR 15
2008 '0', # 27 | Federal Tax*** NUM* 9
2009 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2010 '0', # 29 | Other Taxes & Fees*** NUM* 9
2013 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2015 my ($previous_balance) = $self->previous;
2016 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2018 ($_->{pkgnum} || ''),
2021 } $self->_items_pkg;
2024 $cust_main->agentnum,
2025 $cust_main->agent->agent,
2029 $cust_main->address1,
2030 $cust_main->address2,
2036 time2str("%x", $self->_date),
2050 time2str("%x", $self->_date),
2051 sprintf("%.2f", $self->charged),
2052 ( map { $cust_main->getfield($_) }
2053 qw( first last company address1 address2 city state zip country ) ),
2055 ) or die "can't create csv";
2058 my $header = $csv->string. "\n";
2061 if ( lc($opt{'format'}) eq 'billco' ) {
2064 foreach my $item ( $self->_items_pkg ) {
2067 '', # 1 | N/A-Leave Empty CHAR 2
2068 '', # 2 | N/A-Leave Empty CHAR 15
2069 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2070 $self->invnum, # 4 | Invoice Number CHAR 15
2071 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2072 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2073 $item->{'amount'}, # 7 | Amount NUM* 9
2074 '', # 8 | Line Format Control** CHAR 2
2075 '', # 9 | Grouping Code CHAR 2
2076 '', # 10 | User Defined CHAR 15
2079 $detail .= $csv->string. "\n";
2083 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2089 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2091 my($pkg, $setup, $recur, $sdate, $edate);
2092 if ( $cust_bill_pkg->pkgnum ) {
2094 ($pkg, $setup, $recur, $sdate, $edate) = (
2095 $cust_bill_pkg->part_pkg->pkg,
2096 ( $cust_bill_pkg->setup != 0
2097 ? sprintf("%.2f", $cust_bill_pkg->setup )
2099 ( $cust_bill_pkg->recur != 0
2100 ? sprintf("%.2f", $cust_bill_pkg->recur )
2102 ( $cust_bill_pkg->sdate
2103 ? time2str("%x", $cust_bill_pkg->sdate)
2105 ($cust_bill_pkg->edate
2106 ?time2str("%x", $cust_bill_pkg->edate)
2110 } else { #pkgnum tax
2111 next unless $cust_bill_pkg->setup != 0;
2112 $pkg = $cust_bill_pkg->desc;
2113 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2114 ( $sdate, $edate ) = ( '', '' );
2120 ( map { '' } (1..11) ),
2121 ($pkg, $setup, $recur, $sdate, $edate)
2122 ) or die "can't create csv";
2124 $detail .= $csv->string. "\n";
2130 ( $header, $detail );
2136 Pays this invoice with a compliemntary payment. If there is an error,
2137 returns the error, otherwise returns false.
2143 my $cust_pay = new FS::cust_pay ( {
2144 'invnum' => $self->invnum,
2145 'paid' => $self->owed,
2148 'payinfo' => $self->cust_main->payinfo,
2156 Attempts to pay this invoice with a credit card payment via a
2157 Business::OnlinePayment realtime gateway. See
2158 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2159 for supported processors.
2165 $self->realtime_bop( 'CC', @_ );
2170 Attempts to pay this invoice with an electronic check (ACH) payment via a
2171 Business::OnlinePayment realtime gateway. See
2172 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2173 for supported processors.
2179 $self->realtime_bop( 'ECHECK', @_ );
2184 Attempts to pay this invoice with phone bill (LEC) payment via a
2185 Business::OnlinePayment realtime gateway. See
2186 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2187 for supported processors.
2193 $self->realtime_bop( 'LEC', @_ );
2197 my( $self, $method ) = (shift,shift);
2198 my $conf = $self->conf;
2201 my $cust_main = $self->cust_main;
2202 my $balance = $cust_main->balance;
2203 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2204 $amount = sprintf("%.2f", $amount);
2205 return "not run (balance $balance)" unless $amount > 0;
2207 my $description = 'Internet Services';
2208 if ( $conf->exists('business-onlinepayment-description') ) {
2209 my $dtempl = $conf->config('business-onlinepayment-description');
2211 my $agent_obj = $cust_main->agent
2212 or die "can't retreive agent for $cust_main (agentnum ".
2213 $cust_main->agentnum. ")";
2214 my $agent = $agent_obj->agent;
2215 my $pkgs = join(', ',
2216 map { $_->part_pkg->pkg }
2217 grep { $_->pkgnum } $self->cust_bill_pkg
2219 $description = eval qq("$dtempl");
2222 $cust_main->realtime_bop($method, $amount,
2223 'description' => $description,
2224 'invnum' => $self->invnum,
2225 #this didn't do what we want, it just calls apply_payments_and_credits
2227 'apply_to_invoice' => 1,
2230 #this changes application behavior: auto payments
2231 #triggered against a specific invoice are now applied
2232 #to that invoice instead of oldest open.
2238 =item batch_card OPTION => VALUE...
2240 Adds a payment for this invoice to the pending credit card batch (see
2241 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2242 runs the payment using a realtime gateway.
2247 my ($self, %options) = @_;
2248 my $cust_main = $self->cust_main;
2250 $options{invnum} = $self->invnum;
2252 $cust_main->batch_card(%options);
2255 sub _agent_template {
2257 $self->cust_main->agent_template;
2260 sub _agent_invoice_from {
2262 $self->cust_main->agent_invoice_from;
2265 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2267 Returns an text invoice, as a list of lines.
2269 Options can be passed as a hashref (recommended) or as a list of time, template
2270 and then any key/value pairs for any other options.
2272 I<time>, if specified, is used to control the printing of overdue messages. The
2273 default is now. It isn't the date of the invoice; that's the `_date' field.
2274 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2275 L<Time::Local> and L<Date::Parse> for conversion functions.
2277 I<template>, if specified, is the name of a suffix for alternate invoices.
2279 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2285 my( $today, $template, %opt );
2287 %opt = %{ shift() };
2288 $today = delete($opt{'time'}) || '';
2289 $template = delete($opt{template}) || '';
2291 ( $today, $template, %opt ) = @_;
2294 my %params = ( 'format' => 'template' );
2295 $params{'time'} = $today if $today;
2296 $params{'template'} = $template if $template;
2297 $params{$_} = $opt{$_}
2298 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2300 $self->print_generic( %params );
2303 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2305 Internal method - returns a filename of a filled-in LaTeX template for this
2306 invoice (Note: add ".tex" to get the actual filename), and a filename of
2307 an associated logo (with the .eps extension included).
2309 See print_ps and print_pdf for methods that return PostScript and PDF output.
2311 Options can be passed as a hashref (recommended) or as a list of time, template
2312 and then any key/value pairs for any other options.
2314 I<time>, if specified, is used to control the printing of overdue messages. The
2315 default is now. It isn't the date of the invoice; that's the `_date' field.
2316 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2317 L<Time::Local> and L<Date::Parse> for conversion functions.
2319 I<template>, if specified, is the name of a suffix for alternate invoices.
2321 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2327 my $conf = $self->conf;
2328 my( $today, $template, %opt );
2330 %opt = %{ shift() };
2331 $today = delete($opt{'time'}) || '';
2332 $template = delete($opt{template}) || '';
2334 ( $today, $template, %opt ) = @_;
2337 my %params = ( 'format' => 'latex' );
2338 $params{'time'} = $today if $today;
2339 $params{'template'} = $template if $template;
2340 $params{$_} = $opt{$_}
2341 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2343 $template ||= $self->_agent_template;
2345 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2346 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2350 ) or die "can't open temp file: $!\n";
2352 my $agentnum = $self->cust_main->agentnum;
2354 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2355 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2356 or die "can't write temp file: $!\n";
2358 print $lh $conf->config_binary('logo.eps', $agentnum)
2359 or die "can't write temp file: $!\n";
2362 $params{'logo_file'} = $lh->filename;
2364 if($conf->exists('invoice-barcode')){
2365 my $png_file = $self->invoice_barcode($dir);
2366 my $eps_file = $png_file;
2367 $eps_file =~ s/\.png$/.eps/g;
2368 $png_file =~ /(barcode.*png)/;
2370 $eps_file =~ /(barcode.*eps)/;
2373 my $curr_dir = cwd();
2375 # after painfuly long experimentation, it was determined that sam2p won't
2376 # accept : and other chars in the path, no matter how hard I tried to
2377 # escape them, hence the chdir (and chdir back, just to be safe)
2378 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2379 or die "sam2p failed: $!\n";
2383 $params{'barcode_file'} = $eps_file;
2386 my @filled_in = $self->print_generic( %params );
2388 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2392 ) or die "can't open temp file: $!\n";
2393 binmode($fh, ':utf8'); # language support
2394 print $fh join('', @filled_in );
2397 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2398 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2402 =item invoice_barcode DIR_OR_FALSE
2404 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2405 it is taken as the temp directory where the PNG file will be generated and the
2406 PNG file name is returned. Otherwise, the PNG image itself is returned.
2410 sub invoice_barcode {
2411 my ($self, $dir) = (shift,shift);
2413 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2414 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2415 my $gd = $gdbar->plot(Height => 30);
2418 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2422 ) or die "can't open temp file: $!\n";
2423 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2424 my $png_file = $bh->filename;
2431 =item print_generic OPTION => VALUE ...
2433 Internal method - returns a filled-in template for this invoice as a scalar.
2435 See print_ps and print_pdf for methods that return PostScript and PDF output.
2437 Non optional options include
2438 format - latex, html, template
2440 Optional options include
2442 template - a value used as a suffix for a configuration template
2444 time - a value used to control the printing of overdue messages. The
2445 default is now. It isn't the date of the invoice; that's the `_date' field.
2446 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2447 L<Time::Local> and L<Date::Parse> for conversion functions.
2451 unsquelch_cdr - overrides any per customer cdr squelching when true
2453 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2455 locale - override customer's locale
2459 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2460 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2461 # yes: fixed width/plain text printing will be borked
2463 my( $self, %params ) = @_;
2464 my $conf = $self->conf;
2465 my $today = $params{today} ? $params{today} : time;
2466 warn "$me print_generic called on $self with suffix $params{template}\n"
2469 my $format = $params{format};
2470 die "Unknown format: $format"
2471 unless $format =~ /^(latex|html|template)$/;
2473 my $cust_main = $self->cust_main;
2474 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2475 unless $cust_main->payname
2476 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2478 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2479 'html' => [ '<%=', '%>' ],
2480 'template' => [ '{', '}' ],
2483 warn "$me print_generic creating template\n"
2486 #create the template
2487 my $template = $params{template} ? $params{template} : $self->_agent_template;
2488 my $templatefile = "invoice_$format";
2489 $templatefile .= "_$template"
2490 if length($template) && $conf->exists($templatefile."_$template");
2491 my @invoice_template = map "$_\n", $conf->config($templatefile)
2492 or die "cannot load config data $templatefile";
2495 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2496 #change this to a die when the old code is removed
2497 warn "old-style invoice template $templatefile; ".
2498 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2499 $old_latex = 'true';
2500 @invoice_template = _translate_old_latex_format(@invoice_template);
2503 warn "$me print_generic creating T:T object\n"
2506 my $text_template = new Text::Template(
2508 SOURCE => \@invoice_template,
2509 DELIMITERS => $delimiters{$format},
2512 warn "$me print_generic compiling T:T object\n"
2515 $text_template->compile()
2516 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2519 # additional substitution could possibly cause breakage in existing templates
2520 my %convert_maps = (
2522 'notes' => sub { map "$_", @_ },
2523 'footer' => sub { map "$_", @_ },
2524 'smallfooter' => sub { map "$_", @_ },
2525 'returnaddress' => sub { map "$_", @_ },
2526 'coupon' => sub { map "$_", @_ },
2527 'summary' => sub { map "$_", @_ },
2533 s/%%(.*)$/<!-- $1 -->/g;
2534 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2535 s/\\begin\{enumerate\}/<ol>/g;
2537 s/\\end\{enumerate\}/<\/ol>/g;
2538 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2547 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2549 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2554 s/\\\\\*?\s*$/<BR>/;
2555 s/\\hyphenation\{[\w\s\-]+}//;
2560 'coupon' => sub { "" },
2561 'summary' => sub { "" },
2568 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2569 s/\\begin\{enumerate\}//g;
2571 s/\\end\{enumerate\}//g;
2572 s/\\textbf\{(.*)\}/$1/g;
2579 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2581 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2586 s/\\\\\*?\s*$/\n/; # dubious
2587 s/\\hyphenation\{[\w\s\-]+}//;
2591 'coupon' => sub { "" },
2592 'summary' => sub { "" },
2597 # hashes for differing output formats
2598 my %nbsps = ( 'latex' => '~',
2599 'html' => '', # '&nbps;' would be nice
2600 'template' => '', # not used
2602 my $nbsp = $nbsps{$format};
2604 my %escape_functions = ( 'latex' => \&_latex_escape,
2605 'html' => \&_html_escape_nbsp,#\&encode_entities,
2606 'template' => sub { shift },
2608 my $escape_function = $escape_functions{$format};
2609 my $escape_function_nonbsp = ($format eq 'html')
2610 ? \&_html_escape : $escape_function;
2612 my %date_formats = ( 'latex' => $date_format_long,
2613 'html' => $date_format_long,
2616 $date_formats{'html'} =~ s/ / /g;
2618 my $date_format = $date_formats{$format};
2620 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2622 'html' => sub { return '<b>'. shift(). '</b>'
2624 'template' => sub { shift },
2626 my $embolden_function = $embolden_functions{$format};
2628 my %newline_tokens = ( 'latex' => '\\\\',
2632 my $newline_token = $newline_tokens{$format};
2634 warn "$me generating template variables\n"
2637 # generate template variables
2640 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2644 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2650 $returnaddress = join("\n",
2651 $conf->config_orbase("invoice_${format}returnaddress", $template)
2654 } elsif ( grep /\S/,
2655 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2657 my $convert_map = $convert_maps{$format}{'returnaddress'};
2660 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2665 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2667 my $convert_map = $convert_maps{$format}{'returnaddress'};
2668 $returnaddress = join( "\n", &$convert_map(
2669 map { s/( {2,})/'~' x length($1)/eg;
2673 ( $conf->config('company_name', $self->cust_main->agentnum),
2674 $conf->config('company_address', $self->cust_main->agentnum),
2681 my $warning = "Couldn't find a return address; ".
2682 "do you need to set the company_address configuration value?";
2684 $returnaddress = $nbsp;
2685 #$returnaddress = $warning;
2689 warn "$me generating invoice data\n"
2692 my $agentnum = $self->cust_main->agentnum;
2694 my %invoice_data = (
2697 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2698 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2699 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2700 'returnaddress' => $returnaddress,
2701 'agent' => &$escape_function($cust_main->agent->agent),
2704 'invnum' => $self->invnum,
2705 'date' => time2str($date_format, $self->_date),
2706 'today' => time2str($date_format_long, $today),
2707 'terms' => $self->terms,
2708 'template' => $template, #params{'template'},
2709 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2710 'current_charges' => sprintf("%.2f", $self->charged),
2711 'duedate' => $self->due_date2str($rdate_format), #date_format?
2714 'custnum' => $cust_main->display_custnum,
2715 'agent_custid' => &$escape_function($cust_main->agent_custid),
2716 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2717 payname company address1 address2 city state zip fax
2721 'ship_enable' => $conf->exists('invoice-ship_address'),
2722 'unitprices' => $conf->exists('invoice-unitprice'),
2723 'smallernotes' => $conf->exists('invoice-smallernotes'),
2724 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2725 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2727 #layout info -- would be fancy to calc some of this and bury the template
2729 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2730 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2731 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2732 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2733 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2734 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2735 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2736 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2737 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2738 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2740 # better hang on to conf_dir for a while (for old templates)
2741 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2743 #these are only used when doing paged plaintext
2750 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2751 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2752 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2753 # eval to avoid death for unimplemented languages
2754 my $dh = eval { Date::Language->new($info{'name'}) } ||
2755 Date::Language->new(); # fall back to English
2756 # prototype here to silence warnings
2757 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2758 # eventually use this date handle everywhere in here, too
2760 my $min_sdate = 999999999999;
2762 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2763 next unless $cust_bill_pkg->pkgnum > 0;
2764 $min_sdate = $cust_bill_pkg->sdate
2765 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2766 $max_edate = $cust_bill_pkg->edate
2767 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2770 $invoice_data{'bill_period'} = '';
2771 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2772 . " to " . time2str('%e %h', $max_edate)
2773 if ($max_edate != 0 && $min_sdate != 999999999999);
2775 $invoice_data{finance_section} = '';
2776 if ( $conf->config('finance_pkgclass') ) {
2778 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2779 $invoice_data{finance_section} = $pkg_class->categoryname;
2781 $invoice_data{finance_amount} = '0.00';
2782 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2784 my $countrydefault = $conf->config('countrydefault') || 'US';
2785 foreach ( qw( address1 address2 city state zip country fax) ){
2786 my $method = 'ship_'.$_;
2787 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2789 foreach ( qw( contact company ) ) { #compatibility
2790 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
2792 $invoice_data{'ship_country'} = ''
2793 if ( $invoice_data{'ship_country'} eq $countrydefault );
2795 $invoice_data{'cid'} = $params{'cid'}
2798 if ( $cust_main->country eq $countrydefault ) {
2799 $invoice_data{'country'} = '';
2801 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2805 $invoice_data{'address'} = \@address;
2807 $cust_main->payname.
2808 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2809 ? " (P.O. #". $cust_main->payinfo. ")"
2813 push @address, $cust_main->company
2814 if $cust_main->company;
2815 push @address, $cust_main->address1;
2816 push @address, $cust_main->address2
2817 if $cust_main->address2;
2819 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2820 push @address, $invoice_data{'country'}
2821 if $invoice_data{'country'};
2823 while (scalar(@address) < 5);
2825 $invoice_data{'logo_file'} = $params{'logo_file'}
2826 if $params{'logo_file'};
2827 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2828 if $params{'barcode_file'};
2829 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2830 if $params{'barcode_img'};
2831 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2832 if $params{'barcode_cid'};
2834 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2835 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2836 #my $balance_due = $self->owed + $pr_total - $cr_total;
2837 my $balance_due = $self->owed + $pr_total;
2839 # the customer's current balance as shown on the invoice before this one
2840 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2842 # the change in balance from that invoice to this one
2843 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2845 # the sum of amount owed on all previous invoices
2846 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2848 # the sum of amount owed on all invoices
2849 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2851 # info from customer's last invoice before this one, for some
2853 $invoice_data{'last_bill'} = {};
2854 my $last_bill = $pr_cust_bill[-1];
2856 $invoice_data{'last_bill'} = {
2857 '_date' => $last_bill->_date, #unformatted
2858 # all we need for now
2862 my $summarypage = '';
2863 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2866 $invoice_data{'summarypage'} = $summarypage;
2868 warn "$me substituting variables in notes, footer, smallfooter\n"
2871 my @include = (qw( notes footer smallfooter ));
2872 push @include, 'coupon' unless $params{'no_coupon'};
2873 foreach my $include (@include) {
2875 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2878 if ( $conf->exists($inc_file, $agentnum)
2879 && length( $conf->config($inc_file, $agentnum) ) ) {
2881 @inc_src = $conf->config($inc_file, $agentnum);
2885 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2887 my $convert_map = $convert_maps{$format}{$include};
2889 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2890 s/--\@\]/$delimiters{$format}[1]/g;
2893 &$convert_map( $conf->config($inc_file, $agentnum) );
2897 my $inc_tt = new Text::Template (
2899 SOURCE => [ map "$_\n", @inc_src ],
2900 DELIMITERS => $delimiters{$format},
2901 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2903 unless ( $inc_tt->compile() ) {
2904 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2905 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2909 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2911 $invoice_data{$include} =~ s/\n+$//
2912 if ($format eq 'latex');
2915 # let invoices use either of these as needed
2916 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2917 ? $cust_main->payinfo : '';
2918 $invoice_data{'po_line'} =
2919 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2920 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2923 my %money_chars = ( 'latex' => '',
2924 'html' => $conf->config('money_char') || '$',
2927 my $money_char = $money_chars{$format};
2929 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2930 'html' => $conf->config('money_char') || '$',
2933 my $other_money_char = $other_money_chars{$format};
2934 $invoice_data{'dollar'} = $other_money_char;
2936 my @detail_items = ();
2937 my @total_items = ();
2941 $invoice_data{'detail_items'} = \@detail_items;
2942 $invoice_data{'total_items'} = \@total_items;
2943 $invoice_data{'buf'} = \@buf;
2944 $invoice_data{'sections'} = \@sections;
2946 warn "$me generating sections\n"
2949 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2950 'subtotal' => $other_money_char.
2951 sprintf('%.2f', $pr_total),
2952 'summarized' => '', #why? $summarypage ? 'Y' : '',
2954 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2955 join(' / ', map { $cust_main->balance_date_range(@$_) }
2956 $self->_prior_month30s
2958 if $conf->exists('invoice_include_aging');
2961 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2962 'subtotal' => $taxtotal, # adjusted below
2964 my $tax_weight = _pkg_category($tax_section->{description})
2965 ? _pkg_category($tax_section->{description})->weight
2967 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2968 $tax_section->{'sort_weight'} = $tax_weight;
2971 my $adjusttotal = 0;
2972 my $adjust_section = { 'description' =>
2973 $self->mt('Credits, Payments, and Adjustments'),
2974 'subtotal' => 0, # adjusted below
2976 my $adjust_weight = _pkg_category($adjust_section->{description})
2977 ? _pkg_category($adjust_section->{description})->weight
2979 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2980 $adjust_section->{'sort_weight'} = $adjust_weight;
2982 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2983 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2984 $invoice_data{'multisection'} = $multisection;
2985 my $late_sections = [];
2986 my $extra_sections = [];
2987 my $extra_lines = ();
2989 my $default_section = { 'description' => '',
2994 if ( $multisection ) {
2995 ($extra_sections, $extra_lines) =
2996 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2997 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2999 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3001 push @detail_items, @$extra_lines if $extra_lines;
3003 $self->_items_sections( $late_sections, # this could stand a refactor
3005 $escape_function_nonbsp,
3009 if ($conf->exists('svc_phone_sections')) {
3010 my ($phone_sections, $phone_lines) =
3011 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3012 push @{$late_sections}, @$phone_sections;
3013 push @detail_items, @$phone_lines;
3015 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3016 my ($accountcode_section, $accountcode_lines) =
3017 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3018 if ( scalar(@$accountcode_lines) ) {
3019 push @{$late_sections}, $accountcode_section;
3020 push @detail_items, @$accountcode_lines;
3023 } else {# not multisection
3024 # make a default section
3025 push @sections, $default_section;
3026 # and calculate the finance charge total, since it won't get done otherwise.
3027 # XXX possibly other totals?
3028 # XXX possibly finance_pkgclass should not be used in this manner?
3029 if ( $conf->exists('finance_pkgclass') ) {
3030 my @finance_charges;
3031 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3032 if ( grep { $_->section eq $invoice_data{finance_section} }
3033 $cust_bill_pkg->cust_bill_pkg_display ) {
3034 # I think these are always setup fees, but just to be sure...
3035 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3038 $invoice_data{finance_amount} =
3039 sprintf('%.2f', sum( @finance_charges ) || 0);
3043 unless ( $conf->exists('disable_previous_balance', $agentnum)
3044 || $conf->exists('previous_balance-summary_only')
3048 warn "$me adding previous balances\n"
3051 foreach my $line_item ( $self->_items_previous ) {
3054 ext_description => [],
3056 $detail->{'ref'} = $line_item->{'pkgnum'};
3057 $detail->{'quantity'} = 1;
3058 $detail->{'section'} = $multisection ? $previous_section
3060 $detail->{'description'} = &$escape_function($line_item->{'description'});
3061 if ( exists $line_item->{'ext_description'} ) {
3062 @{$detail->{'ext_description'}} = map {
3063 &$escape_function($_);
3064 } @{$line_item->{'ext_description'}};
3066 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3067 $line_item->{'amount'};
3068 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3070 push @detail_items, $detail;
3071 push @buf, [ $detail->{'description'},
3072 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3078 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3080 push @buf, ['','-----------'];
3081 push @buf, [ $self->mt('Total Previous Balance'),
3082 $money_char. sprintf("%10.2f", $pr_total) ];
3086 if ( $conf->exists('svc_phone-did-summary') ) {
3087 warn "$me adding DID summary\n"
3090 my ($didsummary,$minutes) = $self->_did_summary;
3091 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3093 { 'description' => $didsummary_desc,
3094 'ext_description' => [ $didsummary, $minutes ],
3098 foreach my $section (@sections, @$late_sections) {
3100 warn "$me adding section \n". Dumper($section)
3103 # begin some normalization
3104 $section->{'subtotal'} = $section->{'amount'}
3106 && !exists($section->{subtotal})
3107 && exists($section->{amount});
3109 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3110 if ( $invoice_data{finance_section} &&
3111 $section->{'description'} eq $invoice_data{finance_section} );
3113 $section->{'subtotal'} = $other_money_char.
3114 sprintf('%.2f', $section->{'subtotal'})
3117 # continue some normalization
3118 $section->{'amount'} = $section->{'subtotal'}
3122 if ( $section->{'description'} ) {
3123 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3128 warn "$me setting options\n"
3131 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3133 $options{'section'} = $section if $multisection;
3134 $options{'format'} = $format;
3135 $options{'escape_function'} = $escape_function;
3136 $options{'no_usage'} = 1 unless $unsquelched;
3137 $options{'unsquelched'} = $unsquelched;
3138 $options{'summary_page'} = $summarypage;
3139 $options{'skip_usage'} =
3140 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3141 $options{'multilocation'} = $multilocation;
3142 $options{'multisection'} = $multisection;
3144 warn "$me searching for line items\n"
3147 foreach my $line_item ( $self->_items_pkg(%options) ) {
3149 warn "$me adding line item $line_item\n"
3153 ext_description => [],
3155 $detail->{'ref'} = $line_item->{'pkgnum'};
3156 $detail->{'quantity'} = $line_item->{'quantity'};
3157 $detail->{'section'} = $section;
3158 $detail->{'description'} = &$escape_function($line_item->{'description'});
3159 if ( exists $line_item->{'ext_description'} ) {
3160 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3162 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3163 $line_item->{'amount'};
3164 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3165 $line_item->{'unit_amount'};
3166 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3168 $detail->{'sdate'} = $line_item->{'sdate'};
3169 $detail->{'edate'} = $line_item->{'edate'};
3170 $detail->{'seconds'} = $line_item->{'seconds'};
3172 push @detail_items, $detail;
3173 push @buf, ( [ $detail->{'description'},
3174 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3176 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3180 if ( $section->{'description'} ) {
3181 push @buf, ( ['','-----------'],
3182 [ $section->{'description'}. ' sub-total',
3183 $section->{'subtotal'} # already formatted this
3192 $invoice_data{current_less_finance} =
3193 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3195 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3196 || $conf->exists('previous_balance-summary_only') )
3198 unshift @sections, $previous_section if $pr_total;
3201 warn "$me adding taxes\n"
3204 foreach my $tax ( $self->_items_tax ) {
3206 $taxtotal += $tax->{'amount'};
3208 my $description = &$escape_function( $tax->{'description'} );
3209 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3211 if ( $multisection ) {
3213 my $money = $old_latex ? '' : $money_char;
3214 push @detail_items, {
3215 ext_description => [],
3218 description => $description,
3219 amount => $money. $amount,
3221 section => $tax_section,
3226 push @total_items, {
3227 'total_item' => $description,
3228 'total_amount' => $other_money_char. $amount,
3233 push @buf,[ $description,
3234 $money_char. $amount,
3241 $total->{'total_item'} = $self->mt('Sub-total');
3242 $total->{'total_amount'} =
3243 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3245 if ( $multisection ) {
3246 $tax_section->{'subtotal'} = $other_money_char.
3247 sprintf('%.2f', $taxtotal);
3248 $tax_section->{'pretotal'} = 'New charges sub-total '.
3249 $total->{'total_amount'};
3250 push @sections, $tax_section if $taxtotal;
3252 unshift @total_items, $total;
3255 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3257 push @buf,['','-----------'];
3258 push @buf,[$self->mt(
3259 $conf->exists('disable_previous_balance', $agentnum)
3261 : 'Total New Charges'
3263 $money_char. sprintf("%10.2f",$self->charged) ];
3269 $item = $conf->config('previous_balance-exclude_from_total')
3270 || 'Total New Charges'
3271 if $conf->exists('previous_balance-exclude_from_total');
3272 my $amount = $self->charged +
3273 ( $conf->exists('disable_previous_balance', $agentnum) ||
3274 $conf->exists('previous_balance-exclude_from_total')
3278 $total->{'total_item'} = &$embolden_function($self->mt($item));
3279 $total->{'total_amount'} =
3280 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3281 if ( $multisection ) {
3282 if ( $adjust_section->{'sort_weight'} ) {
3283 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3284 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3286 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3287 $other_money_char. sprintf('%.2f', $self->charged );
3290 push @total_items, $total;
3292 push @buf,['','-----------'];
3295 sprintf( '%10.2f', $amount )
3300 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3301 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3304 my $credittotal = 0;
3305 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3308 $total->{'total_item'} = &$escape_function($credit->{'description'});
3309 $credittotal += $credit->{'amount'};
3310 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3311 $adjusttotal += $credit->{'amount'};
3312 if ( $multisection ) {
3313 my $money = $old_latex ? '' : $money_char;
3314 push @detail_items, {
3315 ext_description => [],
3318 description => &$escape_function($credit->{'description'}),
3319 amount => $money. $credit->{'amount'},
3321 section => $adjust_section,
3324 push @total_items, $total;
3328 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3331 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3332 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3336 my $paymenttotal = 0;
3337 foreach my $payment ( $self->_items_payments ) {
3339 $total->{'total_item'} = &$escape_function($payment->{'description'});
3340 $paymenttotal += $payment->{'amount'};
3341 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3342 $adjusttotal += $payment->{'amount'};
3343 if ( $multisection ) {
3344 my $money = $old_latex ? '' : $money_char;
3345 push @detail_items, {
3346 ext_description => [],
3349 description => &$escape_function($payment->{'description'}),
3350 amount => $money. $payment->{'amount'},
3352 section => $adjust_section,
3355 push @total_items, $total;
3357 push @buf, [ $payment->{'description'},
3358 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3361 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3363 if ( $multisection ) {
3364 $adjust_section->{'subtotal'} = $other_money_char.
3365 sprintf('%.2f', $adjusttotal);
3366 push @sections, $adjust_section
3367 unless $adjust_section->{sort_weight};
3370 # create Balance Due message
3373 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3374 $total->{'total_amount'} =
3375 &$embolden_function(
3376 $other_money_char. sprintf('%.2f', $summarypage
3378 $self->billing_balance
3379 : $self->owed + $pr_total
3382 if ( $multisection && !$adjust_section->{sort_weight} ) {
3383 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3384 $total->{'total_amount'};
3386 push @total_items, $total;
3388 push @buf,['','-----------'];
3389 push @buf,[$self->balance_due_msg, $money_char.
3390 sprintf("%10.2f", $balance_due ) ];
3393 if ( $conf->exists('previous_balance-show_credit')
3394 and $cust_main->balance < 0 ) {
3395 my $credit_total = {
3396 'total_item' => &$embolden_function($self->credit_balance_msg),
3397 'total_amount' => &$embolden_function(
3398 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3401 if ( $multisection ) {
3402 $adjust_section->{'posttotal'} .= $newline_token .
3403 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3406 push @total_items, $credit_total;
3408 push @buf,['','-----------'];
3409 push @buf,[$self->credit_balance_msg, $money_char.
3410 sprintf("%10.2f", -$cust_main->balance ) ];
3414 if ( $multisection ) {
3415 if ($conf->exists('svc_phone_sections')) {
3417 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3418 $total->{'total_amount'} =
3419 &$embolden_function(
3420 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3422 my $last_section = pop @sections;
3423 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3424 $total->{'total_amount'};
3425 push @sections, $last_section;
3427 push @sections, @$late_sections
3431 # make a discounts-available section, even without multisection
3432 if ( $conf->exists('discount-show_available')
3433 and my @discounts_avail = $self->_items_discounts_avail ) {
3434 my $discount_section = {
3435 'description' => $self->mt('Discounts Available'),
3440 push @sections, $discount_section;
3441 push @detail_items, map { +{
3442 'ref' => '', #should this be something else?
3443 'section' => $discount_section,
3444 'description' => &$escape_function( $_->{description} ),
3445 'amount' => $money_char . &$escape_function( $_->{amount} ),
3446 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3447 } } @discounts_avail;
3450 # All sections and items are built; now fill in templates.
3451 my @includelist = ();
3452 push @includelist, 'summary' if $summarypage;
3453 foreach my $include ( @includelist ) {
3455 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3458 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3460 @inc_src = $conf->config($inc_file, $agentnum);
3464 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3466 my $convert_map = $convert_maps{$format}{$include};
3468 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3469 s/--\@\]/$delimiters{$format}[1]/g;
3472 &$convert_map( $conf->config($inc_file, $agentnum) );
3476 my $inc_tt = new Text::Template (
3478 SOURCE => [ map "$_\n", @inc_src ],
3479 DELIMITERS => $delimiters{$format},
3480 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3482 unless ( $inc_tt->compile() ) {
3483 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3484 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3488 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3490 $invoice_data{$include} =~ s/\n+$//
3491 if ($format eq 'latex');
3496 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3497 /invoice_lines\((\d*)\)/;
3498 $invoice_lines += $1 || scalar(@buf);
3501 die "no invoice_lines() functions in template?"
3502 if ( $format eq 'template' && !$wasfunc );
3504 if ($format eq 'template') {
3506 if ( $invoice_lines ) {
3507 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3508 $invoice_data{'total_pages'}++
3509 if scalar(@buf) % $invoice_lines;
3512 #setup subroutine for the template
3513 $invoice_data{invoice_lines} = sub {
3514 my $lines = shift || scalar(@buf);
3526 push @collect, split("\n",
3527 $text_template->fill_in( HASH => \%invoice_data )
3529 $invoice_data{'page'}++;
3531 map "$_\n", @collect;
3533 # this is where we actually create the invoice
3534 warn "filling in template for invoice ". $self->invnum. "\n"
3536 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3539 $text_template->fill_in(HASH => \%invoice_data);
3543 # helper routine for generating date ranges
3544 sub _prior_month30s {
3547 [ 1, 2592000 ], # 0-30 days ago
3548 [ 2592000, 5184000 ], # 30-60 days ago
3549 [ 5184000, 7776000 ], # 60-90 days ago
3550 [ 7776000, 0 ], # 90+ days ago
3553 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3554 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3559 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3561 Returns an postscript invoice, as a scalar.
3563 Options can be passed as a hashref (recommended) or as a list of time, template
3564 and then any key/value pairs for any other options.
3566 I<time> an optional value used to control the printing of overdue messages. The
3567 default is now. It isn't the date of the invoice; that's the `_date' field.
3568 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3569 L<Time::Local> and L<Date::Parse> for conversion functions.
3571 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3578 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3579 my $ps = generate_ps($file);
3581 unlink($barcodefile) if $barcodefile;
3586 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3588 Returns an PDF invoice, as a scalar.
3590 Options can be passed as a hashref (recommended) or as a list of time, template
3591 and then any key/value pairs for any other options.
3593 I<time> an optional value used to control the printing of overdue messages. The
3594 default is now. It isn't the date of the invoice; that's the `_date' field.
3595 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3596 L<Time::Local> and L<Date::Parse> for conversion functions.
3598 I<template>, if specified, is the name of a suffix for alternate invoices.
3600 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3607 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3608 my $pdf = generate_pdf($file);
3610 unlink($barcodefile) if $barcodefile;
3615 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3617 Returns an HTML invoice, as a scalar.
3619 I<time> an optional value used to control the printing of overdue messages. The
3620 default is now. It isn't the date of the invoice; that's the `_date' field.
3621 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3622 L<Time::Local> and L<Date::Parse> for conversion functions.
3624 I<template>, if specified, is the name of a suffix for alternate invoices.
3626 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3628 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3629 when emailing the invoice as part of a multipart/related MIME email.
3637 %params = %{ shift() };
3639 $params{'time'} = shift;
3640 $params{'template'} = shift;
3641 $params{'cid'} = shift;
3644 $params{'format'} = 'html';
3646 $self->print_generic( %params );
3649 # quick subroutine for print_latex
3651 # There are ten characters that LaTeX treats as special characters, which
3652 # means that they do not simply typeset themselves:
3653 # # $ % & ~ _ ^ \ { }
3655 # TeX ignores blanks following an escaped character; if you want a blank (as
3656 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3660 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3661 $value =~ s/([<>])/\$$1\$/g;
3667 encode_entities($value);
3671 sub _html_escape_nbsp {
3672 my $value = _html_escape(shift);
3673 $value =~ s/ +/ /g;
3677 #utility methods for print_*
3679 sub _translate_old_latex_format {
3680 warn "_translate_old_latex_format called\n"
3687 if ( $line =~ /^%%Detail\s*$/ ) {
3689 push @template, q![@--!,
3690 q! foreach my $_tr_line (@detail_items) {!,
3691 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3692 q! $_tr_line->{'description'} .= !,
3693 q! "\\tabularnewline\n~~".!,
3694 q! join( "\\tabularnewline\n~~",!,
3695 q! @{$_tr_line->{'ext_description'}}!,
3699 while ( ( my $line_item_line = shift )
3700 !~ /^%%EndDetail\s*$/ ) {
3701 $line_item_line =~ s/'/\\'/g; # nice LTS
3702 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3703 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3704 push @template, " \$OUT .= '$line_item_line';";
3707 push @template, '}',
3710 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3712 push @template, '[@--',
3713 ' foreach my $_tr_line (@total_items) {';
3715 while ( ( my $total_item_line = shift )
3716 !~ /^%%EndTotalDetails\s*$/ ) {
3717 $total_item_line =~ s/'/\\'/g; # nice LTS
3718 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3719 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3720 push @template, " \$OUT .= '$total_item_line';";
3723 push @template, '}',
3727 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3728 push @template, $line;
3734 warn "$_\n" foreach @template;
3742 my $conf = $self->conf;
3744 #check for an invoice-specific override
3745 return $self->invoice_terms if $self->invoice_terms;
3747 #check for a customer- specific override
3748 my $cust_main = $self->cust_main;
3749 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3751 #use configured default
3752 $conf->config('invoice_default_terms') || '';
3758 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3759 $duedate = $self->_date() + ( $1 * 86400 );
3766 $self->due_date ? time2str(shift, $self->due_date) : '';
3769 sub balance_due_msg {
3771 my $msg = $self->mt('Balance Due');
3772 return $msg unless $self->terms;
3773 if ( $self->due_date ) {
3774 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3775 $self->due_date2str($date_format);
3776 } elsif ( $self->terms ) {
3777 $msg .= ' - '. $self->terms;
3782 sub balance_due_date {
3784 my $conf = $self->conf;
3786 if ( $conf->exists('invoice_default_terms')
3787 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3788 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3793 sub credit_balance_msg {
3795 $self->mt('Credit Balance Remaining')
3798 =item invnum_date_pretty
3800 Returns a string with the invoice number and date, for example:
3801 "Invoice #54 (3/20/2008)"
3805 sub invnum_date_pretty {
3807 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3812 Returns a string with the date, for example: "3/20/2008"
3818 time2str($date_format, $self->_date);
3821 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3823 Generate section information for all items appearing on this invoice.
3824 This will only be called for multi-section invoices.
3826 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3827 related display records (L<FS::cust_bill_pkg_display>) and organize
3828 them into two groups ("early" and "late" according to whether they come
3829 before or after the total), then into sections. A subtotal is calculated
3832 Section descriptions are returned in sort weight order. Each consists
3833 of a hash containing:
3835 description: the package category name, escaped
3836 subtotal: the total charges in that section
3837 tax_section: a flag indicating that the section contains only tax charges
3838 summarized: same as tax_section, for some reason
3839 sort_weight: the package category's sort weight
3841 If 'condense' is set on the display record, it also contains everything
3842 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3843 coderefs to generate parts of the invoice. This is not advised.
3847 LATE: an arrayref to push the "late" section hashes onto. The "early"
3848 group is simply returned from the method.
3850 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3851 Turning this on has the following effects:
3852 - Ignores display items with the 'summary' flag.
3853 - Combines all items into the "early" group.
3854 - Creates sections for all non-disabled package categories, even if they
3855 have no charges on this invoice, as well as a section with no name.
3857 ESCAPE: an escape function to use for section titles.
3859 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3860 sorted list. If there are any of these, section subtotals exclude
3863 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3864 passed through to C<_condense_section()>.
3868 use vars qw(%pkg_category_cache);
3869 sub _items_sections {
3872 my $summarypage = shift;
3874 my $extra_sections = shift;
3878 my %late_subtotal = ();
3881 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3884 my $usage = $cust_bill_pkg->usage;
3886 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3887 next if ( $display->summary && $summarypage );
3889 my $section = $display->section;
3890 my $type = $display->type;
3892 $not_tax{$section} = 1
3893 unless $cust_bill_pkg->pkgnum == 0;
3895 if ( $display->post_total && !$summarypage ) {
3896 if (! $type || $type eq 'S') {
3897 $late_subtotal{$section} += $cust_bill_pkg->setup
3898 if $cust_bill_pkg->setup != 0
3899 || $cust_bill_pkg->setup_show_zero;
3903 $late_subtotal{$section} += $cust_bill_pkg->recur
3904 if $cust_bill_pkg->recur != 0
3905 || $cust_bill_pkg->recur_show_zero;
3908 if ($type && $type eq 'R') {
3909 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3910 if $cust_bill_pkg->recur != 0
3911 || $cust_bill_pkg->recur_show_zero;
3914 if ($type && $type eq 'U') {
3915 $late_subtotal{$section} += $usage
3916 unless scalar(@$extra_sections);
3921 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3923 if (! $type || $type eq 'S') {
3924 $subtotal{$section} += $cust_bill_pkg->setup
3925 if $cust_bill_pkg->setup != 0
3926 || $cust_bill_pkg->setup_show_zero;
3930 $subtotal{$section} += $cust_bill_pkg->recur
3931 if $cust_bill_pkg->recur != 0
3932 || $cust_bill_pkg->recur_show_zero;
3935 if ($type && $type eq 'R') {
3936 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3937 if $cust_bill_pkg->recur != 0
3938 || $cust_bill_pkg->recur_show_zero;
3941 if ($type && $type eq 'U') {
3942 $subtotal{$section} += $usage
3943 unless scalar(@$extra_sections);
3952 %pkg_category_cache = ();
3954 push @$late, map { { 'description' => &{$escape}($_),
3955 'subtotal' => $late_subtotal{$_},
3957 'sort_weight' => ( _pkg_category($_)
3958 ? _pkg_category($_)->weight
3961 ((_pkg_category($_) && _pkg_category($_)->condense)
3962 ? $self->_condense_section($format)
3966 sort _sectionsort keys %late_subtotal;
3969 if ( $summarypage ) {
3970 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3971 map { $_->categoryname } qsearch('pkg_category', {});
3972 push @sections, '' if exists($subtotal{''});
3974 @sections = keys %subtotal;
3977 my @early = map { { 'description' => &{$escape}($_),
3978 'subtotal' => $subtotal{$_},
3979 'summarized' => $not_tax{$_} ? '' : 'Y',
3980 'tax_section' => $not_tax{$_} ? '' : 'Y',
3981 'sort_weight' => ( _pkg_category($_)
3982 ? _pkg_category($_)->weight
3985 ((_pkg_category($_) && _pkg_category($_)->condense)
3986 ? $self->_condense_section($format)
3991 push @early, @$extra_sections if $extra_sections;
3993 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3997 #helper subs for above
4000 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4004 my $categoryname = shift;
4005 $pkg_category_cache{$categoryname} ||=
4006 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4009 my %condensed_format = (
4010 'label' => [ qw( Description Qty Amount ) ],
4012 sub { shift->{description} },
4013 sub { shift->{quantity} },
4014 sub { my($href, %opt) = @_;
4015 ($opt{dollar} || ''). $href->{amount};
4018 'align' => [ qw( l r r ) ],
4019 'span' => [ qw( 5 1 1 ) ], # unitprices?
4020 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4023 sub _condense_section {
4024 my ( $self, $format ) = ( shift, shift );
4026 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4027 qw( description_generator
4030 total_line_generator
4035 sub _condensed_generator_defaults {
4036 my ( $self, $format ) = ( shift, shift );
4037 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4046 sub _condensed_header_generator {
4047 my ( $self, $format ) = ( shift, shift );
4049 my ( $f, $prefix, $suffix, $separator, $column ) =
4050 _condensed_generator_defaults($format);
4052 if ($format eq 'latex') {
4053 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4054 $suffix = "\\\\\n\\hline";
4057 sub { my ($d,$a,$s,$w) = @_;
4058 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4060 } elsif ( $format eq 'html' ) {
4061 $prefix = '<th></th>';
4065 sub { my ($d,$a,$s,$w) = @_;
4066 return qq!<th align="$html_align{$a}">$d</th>!;
4074 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4076 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4079 $prefix. join($separator, @result). $suffix;
4084 sub _condensed_description_generator {
4085 my ( $self, $format ) = ( shift, shift );
4087 my ( $f, $prefix, $suffix, $separator, $column ) =
4088 _condensed_generator_defaults($format);
4090 my $money_char = '$';
4091 if ($format eq 'latex') {
4092 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4094 $separator = " & \n";
4096 sub { my ($d,$a,$s,$w) = @_;
4097 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4099 $money_char = '\\dollar';
4100 }elsif ( $format eq 'html' ) {
4101 $prefix = '"><td align="center"></td>';
4105 sub { my ($d,$a,$s,$w) = @_;
4106 return qq!<td align="$html_align{$a}">$d</td>!;
4108 #$money_char = $conf->config('money_char') || '$';
4109 $money_char = ''; # this is madness
4117 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4119 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4121 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4122 map { $f->{$_}->[$i] } qw(align span width)
4126 $prefix. join( $separator, @result ). $suffix;
4131 sub _condensed_total_generator {
4132 my ( $self, $format ) = ( shift, shift );
4134 my ( $f, $prefix, $suffix, $separator, $column ) =
4135 _condensed_generator_defaults($format);
4138 if ($format eq 'latex') {
4141 $separator = " & \n";
4143 sub { my ($d,$a,$s,$w) = @_;
4144 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4146 }elsif ( $format eq 'html' ) {
4150 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4152 sub { my ($d,$a,$s,$w) = @_;
4153 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4162 # my $r = &{$f->{fields}->[$i]}(@args);
4163 # $r .= ' Total' unless $i;
4165 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4167 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4168 map { $f->{$_}->[$i] } qw(align span width)
4172 $prefix. join( $separator, @result ). $suffix;
4177 =item total_line_generator FORMAT
4179 Returns a coderef used for generation of invoice total line items for this
4180 usage_class. FORMAT is either html or latex
4184 # should not be used: will have issues with hash element names (description vs
4185 # total_item and amount vs total_amount -- another array of functions?
4187 sub _condensed_total_line_generator {
4188 my ( $self, $format ) = ( shift, shift );
4190 my ( $f, $prefix, $suffix, $separator, $column ) =
4191 _condensed_generator_defaults($format);
4194 if ($format eq 'latex') {
4197 $separator = " & \n";
4199 sub { my ($d,$a,$s,$w) = @_;
4200 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4202 }elsif ( $format eq 'html' ) {
4206 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4208 sub { my ($d,$a,$s,$w) = @_;
4209 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4218 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4220 &{$column}( &{$f->{fields}->[$i]}(@args),
4221 map { $f->{$_}->[$i] } qw(align span width)
4225 $prefix. join( $separator, @result ). $suffix;
4230 #sub _items_extra_usage_sections {
4232 # my $escape = shift;
4234 # my %sections = ();
4236 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4237 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4239 # next unless $cust_bill_pkg->pkgnum > 0;
4241 # foreach my $section ( keys %usage_class ) {
4243 # my $usage = $cust_bill_pkg->usage($section);
4245 # next unless $usage && $usage > 0;
4247 # $sections{$section} ||= 0;
4248 # $sections{$section} += $usage;
4254 # map { { 'description' => &{$escape}($_),
4255 # 'subtotal' => $sections{$_},
4256 # 'summarized' => '',
4257 # 'tax_section' => '',
4260 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4264 sub _items_extra_usage_sections {
4266 my $conf = $self->conf;
4274 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4276 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4277 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4278 next unless $cust_bill_pkg->pkgnum > 0;
4280 foreach my $classnum ( keys %usage_class ) {
4281 my $section = $usage_class{$classnum}->classname;
4282 $classnums{$section} = $classnum;
4284 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4285 my $amount = $detail->amount;
4286 next unless $amount && $amount > 0;
4288 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4289 $sections{$section}{amount} += $amount; #subtotal
4290 $sections{$section}{calls}++;
4291 $sections{$section}{duration} += $detail->duration;
4293 my $desc = $detail->regionname;
4294 my $description = $desc;
4295 $description = substr($desc, 0, $maxlength). '...'
4296 if $format eq 'latex' && length($desc) > $maxlength;
4298 $lines{$section}{$desc} ||= {
4299 description => &{$escape}($description),
4300 #pkgpart => $part_pkg->pkgpart,
4301 pkgnum => $cust_bill_pkg->pkgnum,
4306 #unit_amount => $cust_bill_pkg->unitrecur,
4307 quantity => $cust_bill_pkg->quantity,
4308 product_code => 'N/A',
4309 ext_description => [],
4312 $lines{$section}{$desc}{amount} += $amount;
4313 $lines{$section}{$desc}{calls}++;
4314 $lines{$section}{$desc}{duration} += $detail->duration;
4320 my %sectionmap = ();
4321 foreach (keys %sections) {
4322 my $usage_class = $usage_class{$classnums{$_}};
4323 $sectionmap{$_} = { 'description' => &{$escape}($_),
4324 'amount' => $sections{$_}{amount}, #subtotal
4325 'calls' => $sections{$_}{calls},
4326 'duration' => $sections{$_}{duration},
4328 'tax_section' => '',
4329 'sort_weight' => $usage_class->weight,
4330 ( $usage_class->format
4331 ? ( map { $_ => $usage_class->$_($format) }
4332 qw( description_generator header_generator total_generator total_line_generator )
4339 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4343 foreach my $section ( keys %lines ) {
4344 foreach my $line ( keys %{$lines{$section}} ) {
4345 my $l = $lines{$section}{$line};
4346 $l->{section} = $sectionmap{$section};
4347 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4348 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4353 return(\@sections, \@lines);
4359 my $end = $self->_date;
4361 # start at date of previous invoice + 1 second or 0 if no previous invoice
4362 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4363 $start = 0 if !$start;
4366 my $cust_main = $self->cust_main;
4367 my @pkgs = $cust_main->all_pkgs;
4368 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4371 foreach my $pkg ( @pkgs ) {
4372 my @h_cust_svc = $pkg->h_cust_svc($end);
4373 foreach my $h_cust_svc ( @h_cust_svc ) {
4374 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4375 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4377 my $inserted = $h_cust_svc->date_inserted;
4378 my $deleted = $h_cust_svc->date_deleted;
4379 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4381 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4383 # DID either activated or ported in; cannot be both for same DID simultaneously
4384 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4385 && (!$phone_inserted->lnp_status
4386 || $phone_inserted->lnp_status eq ''
4387 || $phone_inserted->lnp_status eq 'native')) {
4390 else { # this one not so clean, should probably move to (h_)svc_phone
4391 my $phone_portedin = qsearchs( 'h_svc_phone',
4392 { 'svcnum' => $h_cust_svc->svcnum,
4393 'lnp_status' => 'portedin' },
4394 FS::h_svc_phone->sql_h_searchs($end),
4396 $num_portedin++ if $phone_portedin;
4399 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4400 if($deleted >= $start && $deleted <= $end && $phone_deleted
4401 && (!$phone_deleted->lnp_status
4402 || $phone_deleted->lnp_status ne 'portingout')) {
4405 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4406 && $phone_deleted->lnp_status
4407 && $phone_deleted->lnp_status eq 'portingout') {
4411 # increment usage minutes
4412 if ( $phone_inserted ) {
4413 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4414 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4417 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4420 # don't look at this service again
4421 push @seen, $h_cust_svc->svcnum;
4425 $minutes = sprintf("%d", $minutes);
4426 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4427 . "$num_deactivated Ported-Out: $num_portedout ",
4428 "Total Minutes: $minutes");
4431 sub _items_accountcode_cdr {
4436 my $section = { 'amount' => 0,
4439 'sort_weight' => '',
4441 'description' => 'Usage by Account Code',
4447 my %accountcodes = ();
4449 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4450 next unless $cust_bill_pkg->pkgnum > 0;
4452 my @header = $cust_bill_pkg->details_header;
4453 next unless scalar(@header);
4454 $section->{'header'} = join(',',@header);
4456 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4458 $section->{'header'} = $detail->formatted('format' => $format)
4459 if($detail->detail eq $section->{'header'});
4461 my $accountcode = $detail->accountcode;
4462 next unless $accountcode;
4464 my $amount = $detail->amount;
4465 next unless $amount && $amount > 0;
4467 $accountcodes{$accountcode} ||= {
4468 description => $accountcode,
4475 product_code => 'N/A',
4476 section => $section,
4477 ext_description => [ $section->{'header'} ],
4481 $section->{'amount'} += $amount;
4482 $accountcodes{$accountcode}{'amount'} += $amount;
4483 $accountcodes{$accountcode}{calls}++;
4484 $accountcodes{$accountcode}{duration} += $detail->duration;
4485 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4489 foreach my $l ( values %accountcodes ) {
4490 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4491 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4492 foreach my $sorted_detail ( @sorted_detail ) {
4493 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4495 delete $l->{detail_temp};
4499 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4501 return ($section,\@sorted_lines);
4504 sub _items_svc_phone_sections {
4506 my $conf = $self->conf;
4514 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4516 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4517 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4519 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4520 next unless $cust_bill_pkg->pkgnum > 0;
4522 my @header = $cust_bill_pkg->details_header;
4523 next unless scalar(@header);
4525 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4527 my $phonenum = $detail->phonenum;
4528 next unless $phonenum;
4530 my $amount = $detail->amount;
4531 next unless $amount && $amount > 0;
4533 $sections{$phonenum} ||= { 'amount' => 0,
4536 'sort_weight' => -1,
4537 'phonenum' => $phonenum,
4539 $sections{$phonenum}{amount} += $amount; #subtotal
4540 $sections{$phonenum}{calls}++;
4541 $sections{$phonenum}{duration} += $detail->duration;
4543 my $desc = $detail->regionname;
4544 my $description = $desc;
4545 $description = substr($desc, 0, $maxlength). '...'
4546 if $format eq 'latex' && length($desc) > $maxlength;
4548 $lines{$phonenum}{$desc} ||= {
4549 description => &{$escape}($description),
4550 #pkgpart => $part_pkg->pkgpart,
4558 product_code => 'N/A',
4559 ext_description => [],
4562 $lines{$phonenum}{$desc}{amount} += $amount;
4563 $lines{$phonenum}{$desc}{calls}++;
4564 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4566 my $line = $usage_class{$detail->classnum}->classname;
4567 $sections{"$phonenum $line"} ||=
4571 'sort_weight' => $usage_class{$detail->classnum}->weight,
4572 'phonenum' => $phonenum,
4573 'header' => [ @header ],
4575 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4576 $sections{"$phonenum $line"}{calls}++;
4577 $sections{"$phonenum $line"}{duration} += $detail->duration;
4579 $lines{"$phonenum $line"}{$desc} ||= {
4580 description => &{$escape}($description),
4581 #pkgpart => $part_pkg->pkgpart,
4589 product_code => 'N/A',
4590 ext_description => [],
4593 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4594 $lines{"$phonenum $line"}{$desc}{calls}++;
4595 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4596 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4597 $detail->formatted('format' => $format);
4602 my %sectionmap = ();
4603 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4604 foreach ( keys %sections ) {
4605 my @header = @{ $sections{$_}{header} || [] };
4607 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4608 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4609 my $usage_class = $summary ? $simple : $usage_simple;
4610 my $ending = $summary ? ' usage charges' : '';
4613 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4615 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4616 'amount' => $sections{$_}{amount}, #subtotal
4617 'calls' => $sections{$_}{calls},
4618 'duration' => $sections{$_}{duration},
4620 'tax_section' => '',
4621 'phonenum' => $sections{$_}{phonenum},
4622 'sort_weight' => $sections{$_}{sort_weight},
4623 'post_total' => $summary, #inspire pagebreak
4625 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4626 qw( description_generator
4629 total_line_generator
4636 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4637 $a->{sort_weight} <=> $b->{sort_weight}
4642 foreach my $section ( keys %lines ) {
4643 foreach my $line ( keys %{$lines{$section}} ) {
4644 my $l = $lines{$section}{$line};
4645 $l->{section} = $sectionmap{$section};
4646 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4647 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4652 if($conf->exists('phone_usage_class_summary')) {
4653 # this only works with Latex
4657 # after this, we'll have only two sections per DID:
4658 # Calls Summary and Calls Detail
4659 foreach my $section ( @sections ) {
4660 if($section->{'post_total'}) {
4661 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4662 $section->{'total_line_generator'} = sub { '' };
4663 $section->{'total_generator'} = sub { '' };
4664 $section->{'header_generator'} = sub { '' };
4665 $section->{'description_generator'} = '';
4666 push @newsections, $section;
4667 my %calls_detail = %$section;
4668 $calls_detail{'post_total'} = '';
4669 $calls_detail{'sort_weight'} = '';
4670 $calls_detail{'description_generator'} = sub { '' };
4671 $calls_detail{'header_generator'} = sub {
4672 return ' & Date/Time & Called Number & Duration & Price'
4673 if $format eq 'latex';
4676 $calls_detail{'description'} = 'Calls Detail: '
4677 . $section->{'phonenum'};
4678 push @newsections, \%calls_detail;
4682 # after this, each usage class is collapsed/summarized into a single
4683 # line under the Calls Summary section
4684 foreach my $newsection ( @newsections ) {
4685 if($newsection->{'post_total'}) { # this means Calls Summary
4686 foreach my $section ( @sections ) {
4687 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4688 && !$section->{'post_total'});
4689 my $newdesc = $section->{'description'};
4690 my $tn = $section->{'phonenum'};
4691 $newdesc =~ s/$tn//g;
4692 my $line = { ext_description => [],
4696 calls => $section->{'calls'},
4697 section => $newsection,
4698 duration => $section->{'duration'},
4699 description => $newdesc,
4700 amount => sprintf("%.2f",$section->{'amount'}),
4701 product_code => 'N/A',
4703 push @newlines, $line;
4708 # after this, Calls Details is populated with all CDRs
4709 foreach my $newsection ( @newsections ) {
4710 if(!$newsection->{'post_total'}) { # this means Calls Details
4711 foreach my $line ( @lines ) {
4712 next unless (scalar(@{$line->{'ext_description'}}) &&
4713 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4715 my @extdesc = @{$line->{'ext_description'}};
4717 foreach my $extdesc ( @extdesc ) {
4718 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4719 push @newextdesc, $extdesc;
4721 $line->{'ext_description'} = \@newextdesc;
4722 $line->{'section'} = $newsection;
4723 push @newlines, $line;
4728 return(\@newsections, \@newlines);
4731 return(\@sections, \@lines);
4735 sub _items { # seems to be unused
4738 #my @display = scalar(@_)
4740 # : qw( _items_previous _items_pkg );
4741 # #: qw( _items_pkg );
4742 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4743 my @display = qw( _items_previous _items_pkg );
4746 foreach my $display ( @display ) {
4747 push @b, $self->$display(@_);
4752 sub _items_previous {
4754 my $conf = $self->conf;
4755 my $cust_main = $self->cust_main;
4756 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4758 foreach ( @pr_cust_bill ) {
4759 my $date = $conf->exists('invoice_show_prior_due_date')
4760 ? 'due '. $_->due_date2str($date_format)
4761 : time2str($date_format, $_->_date);
4763 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4764 #'pkgpart' => 'N/A',
4766 'amount' => sprintf("%.2f", $_->owed),
4772 # 'description' => 'Previous Balance',
4773 # #'pkgpart' => 'N/A',
4774 # 'pkgnum' => 'N/A',
4775 # 'amount' => sprintf("%10.2f", $pr_total ),
4776 # 'ext_description' => [ map {
4777 # "Invoice ". $_->invnum.
4778 # " (". time2str("%x",$_->_date). ") ".
4779 # sprintf("%10.2f", $_->owed)
4780 # } @pr_cust_bill ],
4785 =item _items_pkg [ OPTIONS ]
4787 Return line item hashes for each package item on this invoice. Nearly
4790 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4792 The only OPTIONS accepted is 'section', which may point to a hashref
4793 with a key named 'condensed', which may have a true value. If it
4794 does, this method tries to merge identical items into items with
4795 'quantity' equal to the number of items (not the sum of their
4796 separate quantities, for some reason).
4804 warn "$me _items_pkg searching for all package line items\n"
4807 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4809 warn "$me _items_pkg filtering line items\n"
4811 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4813 if ($options{section} && $options{section}->{condensed}) {
4815 warn "$me _items_pkg condensing section\n"
4819 local $Storable::canonical = 1;
4820 foreach ( @items ) {
4822 delete $item->{ref};
4823 delete $item->{ext_description};
4824 my $key = freeze($item);
4825 $itemshash{$key} ||= 0;
4826 $itemshash{$key} ++; # += $item->{quantity};
4828 @items = sort { $a->{description} cmp $b->{description} }
4829 map { my $i = thaw($_);
4830 $i->{quantity} = $itemshash{$_};
4832 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4838 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4845 return 0 unless $a->itemdesc cmp $b->itemdesc;
4846 return -1 if $b->itemdesc eq 'Tax';
4847 return 1 if $a->itemdesc eq 'Tax';
4848 return -1 if $b->itemdesc eq 'Other surcharges';
4849 return 1 if $a->itemdesc eq 'Other surcharges';
4850 $a->itemdesc cmp $b->itemdesc;
4855 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4856 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4859 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4861 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4862 list of hashrefs describing the line items they generate on the invoice.
4864 OPTIONS may include:
4866 format: the invoice format.
4868 escape_function: the function used to escape strings.
4870 DEPRECATED? (expensive, mostly unused?)
4871 format_function: the function used to format CDRs.
4873 section: a hashref containing 'description'; if this is present,
4874 cust_bill_pkg_display records not belonging to this section are
4877 multisection: a flag indicating that this is a multisection invoice,
4878 which does something complicated.
4880 multilocation: a flag to display the location label for the package.
4882 Returns a list of hashrefs, each of which may contain:
4884 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4885 ext_description, which is an arrayref of detail lines to show below
4890 sub _items_cust_bill_pkg {
4892 my $conf = $self->conf;
4893 my $cust_bill_pkgs = shift;
4896 my $format = $opt{format} || '';
4897 my $escape_function = $opt{escape_function} || sub { shift };
4898 my $format_function = $opt{format_function} || '';
4899 my $no_usage = $opt{no_usage} || '';
4900 my $unsquelched = $opt{unsquelched} || ''; #unused
4901 my $section = $opt{section}->{description} if $opt{section};
4902 my $summary_page = $opt{summary_page} || ''; #unused
4903 my $multilocation = $opt{multilocation} || '';
4904 my $multisection = $opt{multisection} || '';
4905 my $discount_show_always = 0;
4907 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4909 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4912 my ($s, $r, $u) = ( undef, undef, undef );
4913 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4916 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4917 if ( $_ && !$cust_bill_pkg->hidden ) {
4918 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4919 $_->{amount} =~ s/^\-0\.00$/0.00/;
4920 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4922 if $_->{amount} != 0
4923 || $discount_show_always
4924 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4925 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4931 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4933 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4934 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4937 foreach my $display ( grep { defined($section)
4938 ? $_->section eq $section
4941 #grep { !$_->summary || !$summary_page } # bunk!
4942 grep { !$_->summary || $multisection }
4943 @cust_bill_pkg_display
4947 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4948 $display->billpkgdisplaynum. "\n"
4951 my $type = $display->type;
4953 my $desc = $cust_bill_pkg->desc;
4954 $desc = substr($desc, 0, $maxlength). '...'
4955 if $format eq 'latex' && length($desc) > $maxlength;
4957 my %details_opt = ( 'format' => $format,
4958 'escape_function' => $escape_function,
4959 'format_function' => $format_function,
4960 'no_usage' => $opt{'no_usage'},
4963 if ( $cust_bill_pkg->pkgnum > 0 ) {
4965 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4968 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4970 # start/end dates for invoice formats that do nonstandard
4972 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4974 if ( (!$type || $type eq 'S')
4975 && ( $cust_bill_pkg->setup != 0
4976 || $cust_bill_pkg->setup_show_zero
4981 warn "$me _items_cust_bill_pkg adding setup\n"
4984 my $description = $desc;
4985 $description .= ' Setup'
4986 if $cust_bill_pkg->recur != 0
4987 || $discount_show_always
4988 || $cust_bill_pkg->recur_show_zero;
4991 unless ( $cust_pkg->part_pkg->hide_svc_detail
4992 || $cust_bill_pkg->hidden )
4995 push @d, map &{$escape_function}($_),
4996 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4997 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4999 if ( $multilocation ) {
5000 my $loc = $cust_pkg->location_label;
5001 $loc = substr($loc, 0, $maxlength). '...'
5002 if $format eq 'latex' && length($loc) > $maxlength;
5003 push @d, &{$escape_function}($loc);
5006 } #unless hiding service details
5008 push @d, $cust_bill_pkg->details(%details_opt)
5009 if $cust_bill_pkg->recur == 0;
5011 if ( $cust_bill_pkg->hidden ) {
5012 $s->{amount} += $cust_bill_pkg->setup;
5013 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5014 push @{ $s->{ext_description} }, @d;
5018 description => $description,
5019 #pkgpart => $part_pkg->pkgpart,
5020 pkgnum => $cust_bill_pkg->pkgnum,
5021 amount => $cust_bill_pkg->setup,
5022 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5023 unit_amount => $cust_bill_pkg->unitsetup,
5024 quantity => $cust_bill_pkg->quantity,
5025 ext_description => \@d,
5031 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5033 $cust_bill_pkg->recur != 0
5034 || $cust_bill_pkg->setup == 0
5035 || $discount_show_always
5036 || $cust_bill_pkg->recur_show_zero
5041 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5044 my $is_summary = $display->summary;
5045 my $description = ($is_summary && $type && $type eq 'U')
5046 ? "Usage charges" : $desc;
5048 #pry be a bit more efficient to look some of this conf stuff up
5051 $conf->exists('disable_line_item_date_ranges')
5052 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5055 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5056 $cust_main->agentnum
5058 if ( defined($date_style) && $date_style eq 'month_of' ) {
5059 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5060 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5061 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5062 $cust_main->agentnum
5064 $desc .= ' ' unless $desc =~ /\s$/;
5065 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5067 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5068 " - ". time2str($date_format, $cust_bill_pkg->edate);
5070 $description .= " ($time_period)";
5074 my @seconds = (); # for display of usage info
5076 #at least until cust_bill_pkg has "past" ranges in addition to
5077 #the "future" sdate/edate ones... see #3032
5078 my @dates = ( $self->_date );
5079 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5080 push @dates, $prev->sdate if $prev;
5081 push @dates, undef if !$prev;
5083 unless ( $cust_pkg->part_pkg->hide_svc_detail
5084 || $cust_bill_pkg->itemdesc
5085 || $cust_bill_pkg->hidden
5086 || $is_summary && $type && $type eq 'U' )
5089 warn "$me _items_cust_bill_pkg adding service details\n"
5092 push @d, map &{$escape_function}($_),
5093 $cust_pkg->h_labels_short(@dates, 'I')
5094 #$cust_bill_pkg->edate,
5095 #$cust_bill_pkg->sdate)
5096 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5098 warn "$me _items_cust_bill_pkg done adding service details\n"
5101 if ( $multilocation ) {
5102 my $loc = $cust_pkg->location_label;
5103 $loc = substr($loc, 0, $maxlength). '...'
5104 if $format eq 'latex' && length($loc) > $maxlength;
5105 push @d, &{$escape_function}($loc);
5108 # Display of seconds_since_sqlradacct:
5109 # On the invoice, when processing @detail_items, look for a field
5110 # named 'seconds'. This will contain total seconds for each
5111 # service, in the same order as @ext_description. For services
5112 # that don't support this it will show undef.
5113 if ( $conf->exists('svc_acct-usage_seconds')
5114 and ! $cust_bill_pkg->pkgpart_override ) {
5115 foreach my $cust_svc (
5116 $cust_pkg->h_cust_svc(@dates, 'I')
5119 # eval because not having any part_export_usage exports
5120 # is a fatal error, last_bill/_date because that's how
5121 # sqlradius_hour billing does it
5123 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5125 push @seconds, $sec;
5127 } #if svc_acct-usage_seconds
5131 unless ( $is_summary ) {
5132 warn "$me _items_cust_bill_pkg adding details\n"
5135 #instead of omitting details entirely in this case (unwanted side
5136 # effects), just omit CDRs
5137 $details_opt{'no_usage'} = 1
5138 if $type && $type eq 'R';
5140 push @d, $cust_bill_pkg->details(%details_opt);
5143 warn "$me _items_cust_bill_pkg calculating amount\n"
5148 $amount = $cust_bill_pkg->recur;
5149 } elsif ($type eq 'R') {
5150 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5151 } elsif ($type eq 'U') {
5152 $amount = $cust_bill_pkg->usage;
5155 if ( !$type || $type eq 'R' ) {
5157 warn "$me _items_cust_bill_pkg adding recur\n"
5160 if ( $cust_bill_pkg->hidden ) {
5161 $r->{amount} += $amount;
5162 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5163 push @{ $r->{ext_description} }, @d;
5166 description => $description,
5167 #pkgpart => $part_pkg->pkgpart,
5168 pkgnum => $cust_bill_pkg->pkgnum,
5170 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5171 unit_amount => $cust_bill_pkg->unitrecur,
5172 quantity => $cust_bill_pkg->quantity,
5174 ext_description => \@d,
5176 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5179 } else { # $type eq 'U'
5181 warn "$me _items_cust_bill_pkg adding usage\n"
5184 if ( $cust_bill_pkg->hidden ) {
5185 $u->{amount} += $amount;
5186 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5187 push @{ $u->{ext_description} }, @d;
5190 description => $description,
5191 #pkgpart => $part_pkg->pkgpart,
5192 pkgnum => $cust_bill_pkg->pkgnum,
5194 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5195 unit_amount => $cust_bill_pkg->unitrecur,
5196 quantity => $cust_bill_pkg->quantity,
5198 ext_description => \@d,
5203 } # recurring or usage with recurring charge
5205 } else { #pkgnum tax or one-shot line item (??)
5207 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5210 if ( $cust_bill_pkg->setup != 0 ) {
5212 'description' => $desc,
5213 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5216 if ( $cust_bill_pkg->recur != 0 ) {
5218 'description' => "$desc (".
5219 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5220 time2str($date_format, $cust_bill_pkg->edate). ')',
5221 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5229 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5230 && $conf->exists('discount-show-always'));
5234 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5236 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5237 $_->{amount} =~ s/^\-0\.00$/0.00/;
5238 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5240 if $_->{amount} != 0
5241 || $discount_show_always
5242 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5243 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5247 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5254 sub _items_credits {
5255 my( $self, %opt ) = @_;
5256 my $trim_len = $opt{'trim_len'} || 60;
5260 foreach ( $self->cust_credited ) {
5262 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5264 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5265 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5266 $reason = " ($reason) " if $reason;
5269 #'description' => 'Credit ref\#'. $_->crednum.
5270 # " (". time2str("%x",$_->cust_credit->_date) .")".
5272 'description' => $self->mt('Credit applied').' '.
5273 time2str($date_format,$_->cust_credit->_date). $reason,
5274 'amount' => sprintf("%.2f",$_->amount),
5282 sub _items_payments {
5286 #get & print payments
5287 foreach ( $self->cust_bill_pay ) {
5289 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5292 'description' => $self->mt('Payment received').' '.
5293 time2str($date_format,$_->cust_pay->_date ),
5294 'amount' => sprintf("%.2f", $_->amount )
5302 =item _items_discounts_avail
5304 Returns an array of line item hashrefs representing available term discounts
5305 for this invoice. This makes the same assumptions that apply to term
5306 discounts in general: that the package is billed monthly, at a flat rate,
5307 with no usage charges. A prorated first month will be handled, as will
5308 a setup fee if the discount is allowed to apply to setup fees.
5312 sub _items_discounts_avail {
5314 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5316 my %plans = $self->discount_plans;
5318 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5322 my $plan = $plans{$months};
5324 my $term_total = sprintf('%.2f', $plan->discounted_total);
5325 my $percent = sprintf('%.0f',
5326 100 * (1 - $term_total / $plan->base_total) );
5327 my $permonth = sprintf('%.2f', $term_total / $months);
5328 my $detail = $self->mt('discount on item'). ' '.
5329 join(', ', map { "#$_" } $plan->pkgnums)
5332 # discounts for non-integer months don't work anyway
5333 $months = sprintf("%d", $months);
5336 description => $self->mt('Save [_1]% by paying for [_2] months',
5338 amount => $self->mt('[_1] ([_2] per month)',
5339 $term_total, $money_char.$permonth),
5340 ext_description => ($detail || ''),
5343 sort { $b <=> $a } keys %plans;
5347 =item call_details [ OPTION => VALUE ... ]
5349 Returns an array of CSV strings representing the call details for this invoice
5350 The only option available is the boolean prepend_billed_number
5355 my ($self, %opt) = @_;
5357 my $format_function = sub { shift };
5359 if ($opt{prepend_billed_number}) {
5360 $format_function = sub {
5364 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5369 my @details = map { $_->details( 'format_function' => $format_function,
5370 'escape_function' => sub{ return() },
5374 $self->cust_bill_pkg;
5375 my $header = $details[0];
5376 ( $header, grep { $_ ne $header } @details );
5386 =item process_reprint
5390 sub process_reprint {
5391 process_re_X('print', @_);
5394 =item process_reemail
5398 sub process_reemail {
5399 process_re_X('email', @_);
5407 process_re_X('fax', @_);
5415 process_re_X('ftp', @_);
5422 sub process_respool {
5423 process_re_X('spool', @_);
5426 use Storable qw(thaw);
5430 my( $method, $job ) = ( shift, shift );
5431 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5433 my $param = thaw(decode_base64(shift));
5434 warn Dumper($param) if $DEBUG;
5445 my($method, $job, %param ) = @_;
5447 warn "re_X $method for job $job with param:\n".
5448 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5451 #some false laziness w/search/cust_bill.html
5453 my $orderby = 'ORDER BY cust_bill._date';
5455 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5457 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5459 my @cust_bill = qsearch( {
5460 #'select' => "cust_bill.*",
5461 'table' => 'cust_bill',
5462 'addl_from' => $addl_from,
5464 'extra_sql' => $extra_sql,
5465 'order_by' => $orderby,
5469 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5471 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5474 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5475 foreach my $cust_bill ( @cust_bill ) {
5476 $cust_bill->$method();
5478 if ( $job ) { #progressbar foo
5480 if ( time - $min_sec > $last ) {
5481 my $error = $job->update_statustext(
5482 int( 100 * $num / scalar(@cust_bill) )
5484 die $error if $error;
5495 =head1 CLASS METHODS
5501 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5506 my ($class, $start, $end) = @_;
5508 $class->paid_sql($start, $end). ' - '.
5509 $class->credited_sql($start, $end);
5514 Returns an SQL fragment to retreive the net amount (charged minus credited).
5519 my ($class, $start, $end) = @_;
5520 'charged - '. $class->credited_sql($start, $end);
5525 Returns an SQL fragment to retreive the amount paid against this invoice.
5530 my ($class, $start, $end) = @_;
5531 $start &&= "AND cust_bill_pay._date <= $start";
5532 $end &&= "AND cust_bill_pay._date > $end";
5533 $start = '' unless defined($start);
5534 $end = '' unless defined($end);
5535 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5536 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5541 Returns an SQL fragment to retreive the amount credited against this invoice.
5546 my ($class, $start, $end) = @_;
5547 $start &&= "AND cust_credit_bill._date <= $start";
5548 $end &&= "AND cust_credit_bill._date > $end";
5549 $start = '' unless defined($start);
5550 $end = '' unless defined($end);
5551 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5552 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5557 Returns an SQL fragment to retrieve the due date of an invoice.
5558 Currently only supported on PostgreSQL.
5563 my $conf = new FS::Conf;
5567 cust_bill.invoice_terms,
5568 cust_main.invoice_terms,
5569 \''.($conf->config('invoice_default_terms') || '').'\'
5570 ), E\'Net (\\\\d+)\'
5572 ) * 86400 + cust_bill._date'
5575 =item search_sql_where HASHREF
5577 Class method which returns an SQL WHERE fragment to search for parameters
5578 specified in HASHREF. Valid parameters are
5584 List reference of start date, end date, as UNIX timestamps.
5594 List reference of charged limits (exclusive).
5598 List reference of charged limits (exclusive).
5602 flag, return open invoices only
5606 flag, return net invoices only
5610 =item newest_percust
5614 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5618 sub search_sql_where {
5619 my($class, $param) = @_;
5621 warn "$me search_sql_where called with params: \n".
5622 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5628 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5629 push @search, "cust_main.agentnum = $1";
5633 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5634 push @search, "cust_bill.custnum = $1";
5638 if ( $param->{_date} ) {
5639 my($beginning, $ending) = @{$param->{_date}};
5641 push @search, "cust_bill._date >= $beginning",
5642 "cust_bill._date < $ending";
5646 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5647 push @search, "cust_bill.invnum >= $1";
5649 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5650 push @search, "cust_bill.invnum <= $1";
5654 if ( $param->{charged} ) {
5655 my @charged = ref($param->{charged})
5656 ? @{ $param->{charged} }
5657 : ($param->{charged});
5659 push @search, map { s/^charged/cust_bill.charged/; $_; }
5663 my $owed_sql = FS::cust_bill->owed_sql;
5666 if ( $param->{owed} ) {
5667 my @owed = ref($param->{owed})
5668 ? @{ $param->{owed} }
5670 push @search, map { s/^owed/$owed_sql/; $_; }
5675 push @search, "0 != $owed_sql"
5676 if $param->{'open'};
5677 push @search, '0 != '. FS::cust_bill->net_sql
5681 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5682 if $param->{'days'};
5685 if ( $param->{'newest_percust'} ) {
5687 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5688 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5690 my @newest_where = map { my $x = $_;
5691 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5694 grep ! /^cust_main./, @search;
5695 my $newest_where = scalar(@newest_where)
5696 ? ' AND '. join(' AND ', @newest_where)
5700 push @search, "cust_bill._date = (
5701 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5702 WHERE newest_cust_bill.custnum = cust_bill.custnum
5708 #promised_date - also has an option to accept nulls
5709 if ( $param->{promised_date} ) {
5710 my($beginning, $ending, $null) = @{$param->{promised_date}};
5712 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5713 "cust_bill.promised_date < $ending )" .
5714 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5717 #agent virtualization
5718 my $curuser = $FS::CurrentUser::CurrentUser;
5719 if ( $curuser->username eq 'fs_queue'
5720 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5722 my $newuser = qsearchs('access_user', {
5723 'username' => $username,
5727 $curuser = $newuser;
5729 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5732 push @search, $curuser->agentnums_sql;
5734 join(' AND ', @search );
5746 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5747 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base