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 - any of FS::Misc::spool_formats
1758 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1759 customer has the corresponding invoice destinations set (see
1760 L<FS::cust_main_invoice>).
1762 =item agent_spools - if set to a true value, will spool to per-agent files
1763 rather than a single global file
1765 =item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will
1766 append to that spool. L<FS::Cron::upload> will then send the spool file to
1769 =item balanceover - if set, only spools the invoice if the total amount owed on
1770 this invoice and all older invoices is greater than the specified amount.
1777 my($self, %opt) = @_;
1779 my $cust_main = $self->cust_main;
1781 if ( $opt{'dest'} ) {
1782 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1783 $cust_main->invoicing_list;
1784 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1785 || ! keys %invoicing_list;
1788 if ( $opt{'balanceover'} ) {
1790 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1793 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1794 mkdir $spooldir, 0700 unless -d $spooldir;
1796 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1799 if ( $opt{'agent_spools'} ) {
1800 $file = 'agentnum'.$cust_main->agentnum;
1805 if ( $opt{'ftp_targetnum'} ) {
1806 $spooldir .= '/target'.$opt{'ftp_targetnum'};
1807 mkdir $spooldir, 0700 unless -d $spooldir;
1808 } # otherwise it just goes into export.xxx/cust_bill
1810 if ( lc($opt{'format'}) eq 'billco' ) {
1814 $file = "$spooldir/$file.csv";
1816 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1818 open(CSV, ">>$file") or die "can't open $file: $!";
1819 flock(CSV, LOCK_EX);
1824 if ( lc($opt{'format'}) eq 'billco' ) {
1826 flock(CSV, LOCK_UN);
1829 $file =~ s/-header.csv$/-detail.csv/;
1831 open(CSV,">>$file") or die "can't open $file: $!";
1832 flock(CSV, LOCK_EX);
1838 flock(CSV, LOCK_UN);
1845 =item print_csv OPTION => VALUE, ...
1847 Returns CSV data for this invoice.
1851 format - 'default', 'billco', 'oneline', 'bridgestone'
1853 Returns a list consisting of two scalars. The first is a single line of CSV
1854 header information for this invoice. The second is one or more lines of CSV
1855 detail information for this invoice.
1857 If I<format> is not specified or "default", the fields of the CSV file are as
1860 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1861 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1865 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1867 B<record_type> is C<cust_bill> for the initial header line only. The
1868 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1869 fields are filled in.
1871 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1872 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1875 =item invnum - invoice number
1877 =item custnum - customer number
1879 =item _date - invoice date
1881 =item charged - total invoice amount
1883 =item first - customer first name
1885 =item last - customer first name
1887 =item company - company name
1889 =item address1 - address line 1
1891 =item address2 - address line 1
1901 =item pkg - line item description
1903 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1905 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1907 =item sdate - start date for recurring fee
1909 =item edate - end date for recurring fee
1913 If I<format> is "billco", the fields of the header CSV file are as follows:
1915 +-------------------------------------------------------------------+
1916 | FORMAT HEADER FILE |
1917 |-------------------------------------------------------------------|
1918 | Field | Description | Name | Type | Width |
1919 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1920 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1921 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1922 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1923 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1924 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1925 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1926 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1927 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1928 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1929 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1930 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1931 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1932 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1933 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1934 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1935 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1936 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1937 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1938 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1939 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1940 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1941 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1942 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1943 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1944 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1945 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1946 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1947 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1948 +-------+-------------------------------+------------+------+-------+
1950 If I<format> is "billco", the fields of the detail CSV file are as follows:
1952 FORMAT FOR DETAIL FILE
1954 Field | Description | Name | Type | Width
1955 1 | N/A-Leave Empty | RC | CHAR | 2
1956 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1957 3 | Account Number | TRACCTNUM | CHAR | 15
1958 4 | Invoice Number | TRINVOICE | CHAR | 15
1959 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1960 6 | Transaction Detail | DETAILS | CHAR | 100
1961 7 | Amount | AMT | NUM* | 9
1962 8 | Line Format Control** | LNCTRL | CHAR | 2
1963 9 | Grouping Code | GROUP | CHAR | 2
1964 10 | User Defined | ACCT CODE | CHAR | 15
1966 If format is 'oneline', there is no detail file. Each invoice has a
1967 header line only, with the fields:
1969 Agent number, agent name, customer number, first name, last name, address
1970 line 1, address line 2, city, state, zip, invoice date, invoice number,
1971 amount charged, amount due,
1973 and then, for each line item, three columns containing the package number,
1974 description, and amount.
1976 If format is 'bridgestone', there is no detail file. Each invoice has a
1977 header line with the following fields in a fixed-width format:
1979 Customer number (in display format), date, name (first last), company,
1980 address 1, address 2, city, state, zip.
1982 This is a mailing list format, and has no per-invoice fields. To avoid
1983 sending redundant notices, the spooling event should have a "once" or
1984 "once_percust_every" condition.
1989 my($self, %opt) = @_;
1991 eval "use Text::CSV_XS";
1994 my $cust_main = $self->cust_main;
1996 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1998 if ( lc($opt{'format'}) eq 'billco' ) {
2001 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2003 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2005 my( $previous_balance, @unused ) = $self->previous; #previous balance
2007 my $pmt_cr_applied = 0;
2008 $pmt_cr_applied += $_->{'amount'}
2009 foreach ( $self->_items_payments, $self->_items_credits ) ;
2011 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2014 '', # 1 | N/A-Leave Empty CHAR 2
2015 '', # 2 | N/A-Leave Empty CHAR 15
2016 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
2017 $self->invnum, # 4 | Transaction Invoice No CHAR 15
2018 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
2019 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
2020 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2021 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2022 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2023 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2024 '', # 10 | Ancillary Billing Information CHAR 30
2025 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2026 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2029 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2032 $duedate, # 14 | Bill Due Date CHAR 10
2034 $previous_balance, # 15 | Previous Balance NUM* 9
2035 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2036 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2037 $totaldue, # 18 | Total Amt Due NUM* 9
2038 $totaldue, # 19 | Total Amt Due NUM* 9
2039 '', # 20 | 30 Day Aging NUM* 9
2040 '', # 21 | 60 Day Aging NUM* 9
2041 '', # 22 | 90 Day Aging NUM* 9
2042 'N', # 23 | Y/N CHAR 1
2043 '', # 24 | Remittance automation CHAR 100
2044 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2045 $self->custnum, # 26 | Customer Reference Number CHAR 15
2046 '0', # 27 | Federal Tax*** NUM* 9
2047 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2048 '0', # 29 | Other Taxes & Fees*** NUM* 9
2051 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2053 my ($previous_balance) = $self->previous;
2054 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2056 ($_->{pkgnum} || ''),
2059 } $self->_items_pkg;
2062 $cust_main->agentnum,
2063 $cust_main->agent->agent,
2067 $cust_main->address1,
2068 $cust_main->address2,
2074 time2str("%x", $self->_date),
2082 } elsif ( lc($opt{'format'}) eq 'bridgestone' ) {
2084 # bypass the CSV stuff and just return this
2085 my $longdate = time2str('%B %d, %Y', time); #current time, right?
2086 my $zip = $cust_main->zip;
2088 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
2092 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
2094 $cust_main->display_custnum,
2096 uc(substr($cust_main->contact_firstlast,0,30)),
2097 uc(substr($cust_main->company ,0,30)),
2098 uc(substr($cust_main->address1 ,0,30)),
2099 uc(substr($cust_main->address2 ,0,30)),
2100 uc(substr($cust_main->city ,0,20)),
2101 uc($cust_main->state),
2113 time2str("%x", $self->_date),
2114 sprintf("%.2f", $self->charged),
2115 ( map { $cust_main->getfield($_) }
2116 qw( first last company address1 address2 city state zip country ) ),
2118 ) or die "can't create csv";
2121 my $header = $csv->string. "\n";
2124 if ( lc($opt{'format'}) eq 'billco' ) {
2127 foreach my $item ( $self->_items_pkg ) {
2130 '', # 1 | N/A-Leave Empty CHAR 2
2131 '', # 2 | N/A-Leave Empty CHAR 15
2132 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2133 $self->invnum, # 4 | Invoice Number CHAR 15
2134 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2135 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2136 $item->{'amount'}, # 7 | Amount NUM* 9
2137 '', # 8 | Line Format Control** CHAR 2
2138 '', # 9 | Grouping Code CHAR 2
2139 '', # 10 | User Defined CHAR 15
2142 $detail .= $csv->string. "\n";
2146 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2152 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2154 my($pkg, $setup, $recur, $sdate, $edate);
2155 if ( $cust_bill_pkg->pkgnum ) {
2157 ($pkg, $setup, $recur, $sdate, $edate) = (
2158 $cust_bill_pkg->part_pkg->pkg,
2159 ( $cust_bill_pkg->setup != 0
2160 ? sprintf("%.2f", $cust_bill_pkg->setup )
2162 ( $cust_bill_pkg->recur != 0
2163 ? sprintf("%.2f", $cust_bill_pkg->recur )
2165 ( $cust_bill_pkg->sdate
2166 ? time2str("%x", $cust_bill_pkg->sdate)
2168 ($cust_bill_pkg->edate
2169 ?time2str("%x", $cust_bill_pkg->edate)
2173 } else { #pkgnum tax
2174 next unless $cust_bill_pkg->setup != 0;
2175 $pkg = $cust_bill_pkg->desc;
2176 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2177 ( $sdate, $edate ) = ( '', '' );
2183 ( map { '' } (1..11) ),
2184 ($pkg, $setup, $recur, $sdate, $edate)
2185 ) or die "can't create csv";
2187 $detail .= $csv->string. "\n";
2193 ( $header, $detail );
2199 Pays this invoice with a compliemntary payment. If there is an error,
2200 returns the error, otherwise returns false.
2206 my $cust_pay = new FS::cust_pay ( {
2207 'invnum' => $self->invnum,
2208 'paid' => $self->owed,
2211 'payinfo' => $self->cust_main->payinfo,
2219 Attempts to pay this invoice with a credit card payment via a
2220 Business::OnlinePayment realtime gateway. See
2221 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2222 for supported processors.
2228 $self->realtime_bop( 'CC', @_ );
2233 Attempts to pay this invoice with an electronic check (ACH) payment via a
2234 Business::OnlinePayment realtime gateway. See
2235 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2236 for supported processors.
2242 $self->realtime_bop( 'ECHECK', @_ );
2247 Attempts to pay this invoice with phone bill (LEC) payment via a
2248 Business::OnlinePayment realtime gateway. See
2249 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2250 for supported processors.
2256 $self->realtime_bop( 'LEC', @_ );
2260 my( $self, $method ) = (shift,shift);
2261 my $conf = $self->conf;
2264 my $cust_main = $self->cust_main;
2265 my $balance = $cust_main->balance;
2266 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2267 $amount = sprintf("%.2f", $amount);
2268 return "not run (balance $balance)" unless $amount > 0;
2270 my $description = 'Internet Services';
2271 if ( $conf->exists('business-onlinepayment-description') ) {
2272 my $dtempl = $conf->config('business-onlinepayment-description');
2274 my $agent_obj = $cust_main->agent
2275 or die "can't retreive agent for $cust_main (agentnum ".
2276 $cust_main->agentnum. ")";
2277 my $agent = $agent_obj->agent;
2278 my $pkgs = join(', ',
2279 map { $_->part_pkg->pkg }
2280 grep { $_->pkgnum } $self->cust_bill_pkg
2282 $description = eval qq("$dtempl");
2285 $cust_main->realtime_bop($method, $amount,
2286 'description' => $description,
2287 'invnum' => $self->invnum,
2288 #this didn't do what we want, it just calls apply_payments_and_credits
2290 'apply_to_invoice' => 1,
2293 #this changes application behavior: auto payments
2294 #triggered against a specific invoice are now applied
2295 #to that invoice instead of oldest open.
2301 =item batch_card OPTION => VALUE...
2303 Adds a payment for this invoice to the pending credit card batch (see
2304 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2305 runs the payment using a realtime gateway.
2310 my ($self, %options) = @_;
2311 my $cust_main = $self->cust_main;
2313 $options{invnum} = $self->invnum;
2315 $cust_main->batch_card(%options);
2318 sub _agent_template {
2320 $self->cust_main->agent_template;
2323 sub _agent_invoice_from {
2325 $self->cust_main->agent_invoice_from;
2328 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2330 Returns an text invoice, as a list of lines.
2332 Options can be passed as a hashref (recommended) or as a list of time, template
2333 and then any key/value pairs for any other options.
2335 I<time>, if specified, is used to control the printing of overdue messages. The
2336 default is now. It isn't the date of the invoice; that's the `_date' field.
2337 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2338 L<Time::Local> and L<Date::Parse> for conversion functions.
2340 I<template>, if specified, is the name of a suffix for alternate invoices.
2342 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2348 my( $today, $template, %opt );
2350 %opt = %{ shift() };
2351 $today = delete($opt{'time'}) || '';
2352 $template = delete($opt{template}) || '';
2354 ( $today, $template, %opt ) = @_;
2357 my %params = ( 'format' => 'template' );
2358 $params{'time'} = $today if $today;
2359 $params{'template'} = $template if $template;
2360 $params{$_} = $opt{$_}
2361 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2363 $self->print_generic( %params );
2366 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2368 Internal method - returns a filename of a filled-in LaTeX template for this
2369 invoice (Note: add ".tex" to get the actual filename), and a filename of
2370 an associated logo (with the .eps extension included).
2372 See print_ps and print_pdf for methods that return PostScript and PDF output.
2374 Options can be passed as a hashref (recommended) or as a list of time, template
2375 and then any key/value pairs for any other options.
2377 I<time>, if specified, is used to control the printing of overdue messages. The
2378 default is now. It isn't the date of the invoice; that's the `_date' field.
2379 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2380 L<Time::Local> and L<Date::Parse> for conversion functions.
2382 I<template>, if specified, is the name of a suffix for alternate invoices.
2384 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2390 my $conf = $self->conf;
2391 my( $today, $template, %opt );
2393 %opt = %{ shift() };
2394 $today = delete($opt{'time'}) || '';
2395 $template = delete($opt{template}) || '';
2397 ( $today, $template, %opt ) = @_;
2400 my %params = ( 'format' => 'latex' );
2401 $params{'time'} = $today if $today;
2402 $params{'template'} = $template if $template;
2403 $params{$_} = $opt{$_}
2404 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2406 $template ||= $self->_agent_template;
2408 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2409 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2413 ) or die "can't open temp file: $!\n";
2415 my $agentnum = $self->cust_main->agentnum;
2417 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2418 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2419 or die "can't write temp file: $!\n";
2421 print $lh $conf->config_binary('logo.eps', $agentnum)
2422 or die "can't write temp file: $!\n";
2425 $params{'logo_file'} = $lh->filename;
2427 if($conf->exists('invoice-barcode')){
2428 my $png_file = $self->invoice_barcode($dir);
2429 my $eps_file = $png_file;
2430 $eps_file =~ s/\.png$/.eps/g;
2431 $png_file =~ /(barcode.*png)/;
2433 $eps_file =~ /(barcode.*eps)/;
2436 my $curr_dir = cwd();
2438 # after painfuly long experimentation, it was determined that sam2p won't
2439 # accept : and other chars in the path, no matter how hard I tried to
2440 # escape them, hence the chdir (and chdir back, just to be safe)
2441 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2442 or die "sam2p failed: $!\n";
2446 $params{'barcode_file'} = $eps_file;
2449 my @filled_in = $self->print_generic( %params );
2451 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2455 ) or die "can't open temp file: $!\n";
2456 binmode($fh, ':utf8'); # language support
2457 print $fh join('', @filled_in );
2460 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2461 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2465 =item invoice_barcode DIR_OR_FALSE
2467 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2468 it is taken as the temp directory where the PNG file will be generated and the
2469 PNG file name is returned. Otherwise, the PNG image itself is returned.
2473 sub invoice_barcode {
2474 my ($self, $dir) = (shift,shift);
2476 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2477 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2478 my $gd = $gdbar->plot(Height => 30);
2481 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2485 ) or die "can't open temp file: $!\n";
2486 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2487 my $png_file = $bh->filename;
2494 =item print_generic OPTION => VALUE ...
2496 Internal method - returns a filled-in template for this invoice as a scalar.
2498 See print_ps and print_pdf for methods that return PostScript and PDF output.
2500 Non optional options include
2501 format - latex, html, template
2503 Optional options include
2505 template - a value used as a suffix for a configuration template
2507 time - a value used to control the printing of overdue messages. The
2508 default is now. It isn't the date of the invoice; that's the `_date' field.
2509 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2510 L<Time::Local> and L<Date::Parse> for conversion functions.
2514 unsquelch_cdr - overrides any per customer cdr squelching when true
2516 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2518 locale - override customer's locale
2522 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2523 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2524 # yes: fixed width/plain text printing will be borked
2526 my( $self, %params ) = @_;
2527 my $conf = $self->conf;
2528 my $today = $params{today} ? $params{today} : time;
2529 warn "$me print_generic called on $self with suffix $params{template}\n"
2532 my $format = $params{format};
2533 die "Unknown format: $format"
2534 unless $format =~ /^(latex|html|template)$/;
2536 my $cust_main = $self->cust_main;
2537 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2538 unless $cust_main->payname
2539 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2541 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2542 'html' => [ '<%=', '%>' ],
2543 'template' => [ '{', '}' ],
2546 warn "$me print_generic creating template\n"
2549 #create the template
2550 my $template = $params{template} ? $params{template} : $self->_agent_template;
2551 my $templatefile = "invoice_$format";
2552 $templatefile .= "_$template"
2553 if length($template) && $conf->exists($templatefile."_$template");
2554 my @invoice_template = map "$_\n", $conf->config($templatefile)
2555 or die "cannot load config data $templatefile";
2558 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2559 #change this to a die when the old code is removed
2560 warn "old-style invoice template $templatefile; ".
2561 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2562 $old_latex = 'true';
2563 @invoice_template = _translate_old_latex_format(@invoice_template);
2566 warn "$me print_generic creating T:T object\n"
2569 my $text_template = new Text::Template(
2571 SOURCE => \@invoice_template,
2572 DELIMITERS => $delimiters{$format},
2575 warn "$me print_generic compiling T:T object\n"
2578 $text_template->compile()
2579 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2582 # additional substitution could possibly cause breakage in existing templates
2583 my %convert_maps = (
2585 'notes' => sub { map "$_", @_ },
2586 'footer' => sub { map "$_", @_ },
2587 'smallfooter' => sub { map "$_", @_ },
2588 'returnaddress' => sub { map "$_", @_ },
2589 'coupon' => sub { map "$_", @_ },
2590 'summary' => sub { map "$_", @_ },
2596 s/%%(.*)$/<!-- $1 -->/g;
2597 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2598 s/\\begin\{enumerate\}/<ol>/g;
2600 s/\\end\{enumerate\}/<\/ol>/g;
2601 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2610 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2612 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2617 s/\\\\\*?\s*$/<BR>/;
2618 s/\\hyphenation\{[\w\s\-]+}//;
2623 'coupon' => sub { "" },
2624 'summary' => sub { "" },
2631 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2632 s/\\begin\{enumerate\}//g;
2634 s/\\end\{enumerate\}//g;
2635 s/\\textbf\{(.*)\}/$1/g;
2642 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2644 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2649 s/\\\\\*?\s*$/\n/; # dubious
2650 s/\\hyphenation\{[\w\s\-]+}//;
2654 'coupon' => sub { "" },
2655 'summary' => sub { "" },
2660 # hashes for differing output formats
2661 my %nbsps = ( 'latex' => '~',
2662 'html' => '', # '&nbps;' would be nice
2663 'template' => '', # not used
2665 my $nbsp = $nbsps{$format};
2667 my %escape_functions = ( 'latex' => \&_latex_escape,
2668 'html' => \&_html_escape_nbsp,#\&encode_entities,
2669 'template' => sub { shift },
2671 my $escape_function = $escape_functions{$format};
2672 my $escape_function_nonbsp = ($format eq 'html')
2673 ? \&_html_escape : $escape_function;
2675 my %date_formats = ( 'latex' => $date_format_long,
2676 'html' => $date_format_long,
2679 $date_formats{'html'} =~ s/ / /g;
2681 my $date_format = $date_formats{$format};
2683 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2685 'html' => sub { return '<b>'. shift(). '</b>'
2687 'template' => sub { shift },
2689 my $embolden_function = $embolden_functions{$format};
2691 my %newline_tokens = ( 'latex' => '\\\\',
2695 my $newline_token = $newline_tokens{$format};
2697 warn "$me generating template variables\n"
2700 # generate template variables
2703 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2707 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2713 $returnaddress = join("\n",
2714 $conf->config_orbase("invoice_${format}returnaddress", $template)
2717 } elsif ( grep /\S/,
2718 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2720 my $convert_map = $convert_maps{$format}{'returnaddress'};
2723 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2728 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2730 my $convert_map = $convert_maps{$format}{'returnaddress'};
2731 $returnaddress = join( "\n", &$convert_map(
2732 map { s/( {2,})/'~' x length($1)/eg;
2736 ( $conf->config('company_name', $self->cust_main->agentnum),
2737 $conf->config('company_address', $self->cust_main->agentnum),
2744 my $warning = "Couldn't find a return address; ".
2745 "do you need to set the company_address configuration value?";
2747 $returnaddress = $nbsp;
2748 #$returnaddress = $warning;
2752 warn "$me generating invoice data\n"
2755 my $agentnum = $self->cust_main->agentnum;
2757 my %invoice_data = (
2760 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2761 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2762 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2763 'returnaddress' => $returnaddress,
2764 'agent' => &$escape_function($cust_main->agent->agent),
2767 'invnum' => $self->invnum,
2768 'date' => time2str($date_format, $self->_date),
2769 'today' => time2str($date_format_long, $today),
2770 'terms' => $self->terms,
2771 'template' => $template, #params{'template'},
2772 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2773 'current_charges' => sprintf("%.2f", $self->charged),
2774 'duedate' => $self->due_date2str($rdate_format), #date_format?
2777 'custnum' => $cust_main->display_custnum,
2778 'agent_custid' => &$escape_function($cust_main->agent_custid),
2779 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2780 payname company address1 address2 city state zip fax
2784 'ship_enable' => $conf->exists('invoice-ship_address'),
2785 'unitprices' => $conf->exists('invoice-unitprice'),
2786 'smallernotes' => $conf->exists('invoice-smallernotes'),
2787 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2788 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2790 #layout info -- would be fancy to calc some of this and bury the template
2792 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2793 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2794 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2795 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2796 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2797 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2798 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2799 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2800 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2801 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2803 # better hang on to conf_dir for a while (for old templates)
2804 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2806 #these are only used when doing paged plaintext
2813 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2814 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2815 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2816 # eval to avoid death for unimplemented languages
2817 my $dh = eval { Date::Language->new($info{'name'}) } ||
2818 Date::Language->new(); # fall back to English
2819 # prototype here to silence warnings
2820 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2821 # eventually use this date handle everywhere in here, too
2823 my $min_sdate = 999999999999;
2825 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2826 next unless $cust_bill_pkg->pkgnum > 0;
2827 $min_sdate = $cust_bill_pkg->sdate
2828 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2829 $max_edate = $cust_bill_pkg->edate
2830 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2833 $invoice_data{'bill_period'} = '';
2834 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2835 . " to " . time2str('%e %h', $max_edate)
2836 if ($max_edate != 0 && $min_sdate != 999999999999);
2838 $invoice_data{finance_section} = '';
2839 if ( $conf->config('finance_pkgclass') ) {
2841 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2842 $invoice_data{finance_section} = $pkg_class->categoryname;
2844 $invoice_data{finance_amount} = '0.00';
2845 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2847 my $countrydefault = $conf->config('countrydefault') || 'US';
2848 foreach ( qw( address1 address2 city state zip country fax) ){
2849 my $method = 'ship_'.$_;
2850 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2852 foreach ( qw( contact company ) ) { #compatibility
2853 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
2855 $invoice_data{'ship_country'} = ''
2856 if ( $invoice_data{'ship_country'} eq $countrydefault );
2858 $invoice_data{'cid'} = $params{'cid'}
2861 if ( $cust_main->country eq $countrydefault ) {
2862 $invoice_data{'country'} = '';
2864 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2868 $invoice_data{'address'} = \@address;
2870 $cust_main->payname.
2871 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2872 ? " (P.O. #". $cust_main->payinfo. ")"
2876 push @address, $cust_main->company
2877 if $cust_main->company;
2878 push @address, $cust_main->address1;
2879 push @address, $cust_main->address2
2880 if $cust_main->address2;
2882 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2883 push @address, $invoice_data{'country'}
2884 if $invoice_data{'country'};
2886 while (scalar(@address) < 5);
2888 $invoice_data{'logo_file'} = $params{'logo_file'}
2889 if $params{'logo_file'};
2890 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2891 if $params{'barcode_file'};
2892 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2893 if $params{'barcode_img'};
2894 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2895 if $params{'barcode_cid'};
2897 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2898 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2899 #my $balance_due = $self->owed + $pr_total - $cr_total;
2900 my $balance_due = $self->owed + $pr_total;
2902 # the customer's current balance as shown on the invoice before this one
2903 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2905 # the change in balance from that invoice to this one
2906 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2908 # the sum of amount owed on all previous invoices
2909 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2911 # the sum of amount owed on all invoices
2912 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2914 # info from customer's last invoice before this one, for some
2916 $invoice_data{'last_bill'} = {};
2917 my $last_bill = $pr_cust_bill[-1];
2919 $invoice_data{'last_bill'} = {
2920 '_date' => $last_bill->_date, #unformatted
2921 # all we need for now
2925 my $summarypage = '';
2926 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2929 $invoice_data{'summarypage'} = $summarypage;
2931 warn "$me substituting variables in notes, footer, smallfooter\n"
2934 my @include = (qw( notes footer smallfooter ));
2935 push @include, 'coupon' unless $params{'no_coupon'};
2936 foreach my $include (@include) {
2938 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2941 if ( $conf->exists($inc_file, $agentnum)
2942 && length( $conf->config($inc_file, $agentnum) ) ) {
2944 @inc_src = $conf->config($inc_file, $agentnum);
2948 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2950 my $convert_map = $convert_maps{$format}{$include};
2952 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2953 s/--\@\]/$delimiters{$format}[1]/g;
2956 &$convert_map( $conf->config($inc_file, $agentnum) );
2960 my $inc_tt = new Text::Template (
2962 SOURCE => [ map "$_\n", @inc_src ],
2963 DELIMITERS => $delimiters{$format},
2964 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2966 unless ( $inc_tt->compile() ) {
2967 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2968 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2972 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2974 $invoice_data{$include} =~ s/\n+$//
2975 if ($format eq 'latex');
2978 # let invoices use either of these as needed
2979 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2980 ? $cust_main->payinfo : '';
2981 $invoice_data{'po_line'} =
2982 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2983 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2986 my %money_chars = ( 'latex' => '',
2987 'html' => $conf->config('money_char') || '$',
2990 my $money_char = $money_chars{$format};
2992 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2993 'html' => $conf->config('money_char') || '$',
2996 my $other_money_char = $other_money_chars{$format};
2997 $invoice_data{'dollar'} = $other_money_char;
2999 my @detail_items = ();
3000 my @total_items = ();
3004 $invoice_data{'detail_items'} = \@detail_items;
3005 $invoice_data{'total_items'} = \@total_items;
3006 $invoice_data{'buf'} = \@buf;
3007 $invoice_data{'sections'} = \@sections;
3009 warn "$me generating sections\n"
3012 my $previous_section = { 'description' => $self->mt('Previous Charges'),
3013 'subtotal' => $other_money_char.
3014 sprintf('%.2f', $pr_total),
3015 'summarized' => '', #why? $summarypage ? 'Y' : '',
3017 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
3018 join(' / ', map { $cust_main->balance_date_range(@$_) }
3019 $self->_prior_month30s
3021 if $conf->exists('invoice_include_aging');
3024 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
3025 'subtotal' => $taxtotal, # adjusted below
3027 my $tax_weight = _pkg_category($tax_section->{description})
3028 ? _pkg_category($tax_section->{description})->weight
3030 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3031 $tax_section->{'sort_weight'} = $tax_weight;
3034 my $adjusttotal = 0;
3035 my $adjust_section = { 'description' =>
3036 $self->mt('Credits, Payments, and Adjustments'),
3037 'subtotal' => 0, # adjusted below
3039 my $adjust_weight = _pkg_category($adjust_section->{description})
3040 ? _pkg_category($adjust_section->{description})->weight
3042 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3043 $adjust_section->{'sort_weight'} = $adjust_weight;
3045 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3046 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3047 $invoice_data{'multisection'} = $multisection;
3048 my $late_sections = [];
3049 my $extra_sections = [];
3050 my $extra_lines = ();
3052 my $default_section = { 'description' => '',
3057 if ( $multisection ) {
3058 ($extra_sections, $extra_lines) =
3059 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3060 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3062 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3064 push @detail_items, @$extra_lines if $extra_lines;
3066 $self->_items_sections( $late_sections, # this could stand a refactor
3068 $escape_function_nonbsp,
3072 if ($conf->exists('svc_phone_sections')) {
3073 my ($phone_sections, $phone_lines) =
3074 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3075 push @{$late_sections}, @$phone_sections;
3076 push @detail_items, @$phone_lines;
3078 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3079 my ($accountcode_section, $accountcode_lines) =
3080 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3081 if ( scalar(@$accountcode_lines) ) {
3082 push @{$late_sections}, $accountcode_section;
3083 push @detail_items, @$accountcode_lines;
3086 } else {# not multisection
3087 # make a default section
3088 push @sections, $default_section;
3089 # and calculate the finance charge total, since it won't get done otherwise.
3090 # XXX possibly other totals?
3091 # XXX possibly finance_pkgclass should not be used in this manner?
3092 if ( $conf->exists('finance_pkgclass') ) {
3093 my @finance_charges;
3094 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3095 if ( grep { $_->section eq $invoice_data{finance_section} }
3096 $cust_bill_pkg->cust_bill_pkg_display ) {
3097 # I think these are always setup fees, but just to be sure...
3098 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3101 $invoice_data{finance_amount} =
3102 sprintf('%.2f', sum( @finance_charges ) || 0);
3106 unless ( $conf->exists('disable_previous_balance', $agentnum)
3107 || $conf->exists('previous_balance-summary_only')
3111 warn "$me adding previous balances\n"
3114 foreach my $line_item ( $self->_items_previous ) {
3117 ext_description => [],
3119 $detail->{'ref'} = $line_item->{'pkgnum'};
3120 $detail->{'quantity'} = 1;
3121 $detail->{'section'} = $multisection ? $previous_section
3123 $detail->{'description'} = &$escape_function($line_item->{'description'});
3124 if ( exists $line_item->{'ext_description'} ) {
3125 @{$detail->{'ext_description'}} = map {
3126 &$escape_function($_);
3127 } @{$line_item->{'ext_description'}};
3129 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3130 $line_item->{'amount'};
3131 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3133 push @detail_items, $detail;
3134 push @buf, [ $detail->{'description'},
3135 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3141 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3143 push @buf, ['','-----------'];
3144 push @buf, [ $self->mt('Total Previous Balance'),
3145 $money_char. sprintf("%10.2f", $pr_total) ];
3149 if ( $conf->exists('svc_phone-did-summary') ) {
3150 warn "$me adding DID summary\n"
3153 my ($didsummary,$minutes) = $self->_did_summary;
3154 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3156 { 'description' => $didsummary_desc,
3157 'ext_description' => [ $didsummary, $minutes ],
3161 foreach my $section (@sections, @$late_sections) {
3163 warn "$me adding section \n". Dumper($section)
3166 # begin some normalization
3167 $section->{'subtotal'} = $section->{'amount'}
3169 && !exists($section->{subtotal})
3170 && exists($section->{amount});
3172 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3173 if ( $invoice_data{finance_section} &&
3174 $section->{'description'} eq $invoice_data{finance_section} );
3176 $section->{'subtotal'} = $other_money_char.
3177 sprintf('%.2f', $section->{'subtotal'})
3180 # continue some normalization
3181 $section->{'amount'} = $section->{'subtotal'}
3185 if ( $section->{'description'} ) {
3186 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3191 warn "$me setting options\n"
3194 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3196 $options{'section'} = $section if $multisection;
3197 $options{'format'} = $format;
3198 $options{'escape_function'} = $escape_function;
3199 $options{'no_usage'} = 1 unless $unsquelched;
3200 $options{'unsquelched'} = $unsquelched;
3201 $options{'summary_page'} = $summarypage;
3202 $options{'skip_usage'} =
3203 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3204 $options{'multilocation'} = $multilocation;
3205 $options{'multisection'} = $multisection;
3207 warn "$me searching for line items\n"
3210 foreach my $line_item ( $self->_items_pkg(%options) ) {
3212 warn "$me adding line item $line_item\n"
3216 ext_description => [],
3218 $detail->{'ref'} = $line_item->{'pkgnum'};
3219 $detail->{'quantity'} = $line_item->{'quantity'};
3220 $detail->{'section'} = $section;
3221 $detail->{'description'} = &$escape_function($line_item->{'description'});
3222 if ( exists $line_item->{'ext_description'} ) {
3223 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3225 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3226 $line_item->{'amount'};
3227 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3228 $line_item->{'unit_amount'};
3229 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3231 $detail->{'sdate'} = $line_item->{'sdate'};
3232 $detail->{'edate'} = $line_item->{'edate'};
3233 $detail->{'seconds'} = $line_item->{'seconds'};
3235 push @detail_items, $detail;
3236 push @buf, ( [ $detail->{'description'},
3237 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3239 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3243 if ( $section->{'description'} ) {
3244 push @buf, ( ['','-----------'],
3245 [ $section->{'description'}. ' sub-total',
3246 $section->{'subtotal'} # already formatted this
3255 $invoice_data{current_less_finance} =
3256 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3258 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3259 || $conf->exists('previous_balance-summary_only') )
3261 unshift @sections, $previous_section if $pr_total;
3264 warn "$me adding taxes\n"
3267 foreach my $tax ( $self->_items_tax ) {
3269 $taxtotal += $tax->{'amount'};
3271 my $description = &$escape_function( $tax->{'description'} );
3272 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3274 if ( $multisection ) {
3276 my $money = $old_latex ? '' : $money_char;
3277 push @detail_items, {
3278 ext_description => [],
3281 description => $description,
3282 amount => $money. $amount,
3284 section => $tax_section,
3289 push @total_items, {
3290 'total_item' => $description,
3291 'total_amount' => $other_money_char. $amount,
3296 push @buf,[ $description,
3297 $money_char. $amount,
3304 $total->{'total_item'} = $self->mt('Sub-total');
3305 $total->{'total_amount'} =
3306 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3308 if ( $multisection ) {
3309 $tax_section->{'subtotal'} = $other_money_char.
3310 sprintf('%.2f', $taxtotal);
3311 $tax_section->{'pretotal'} = 'New charges sub-total '.
3312 $total->{'total_amount'};
3313 push @sections, $tax_section if $taxtotal;
3315 unshift @total_items, $total;
3318 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3320 push @buf,['','-----------'];
3321 push @buf,[$self->mt(
3322 $conf->exists('disable_previous_balance', $agentnum)
3324 : 'Total New Charges'
3326 $money_char. sprintf("%10.2f",$self->charged) ];
3332 $item = $conf->config('previous_balance-exclude_from_total')
3333 || 'Total New Charges'
3334 if $conf->exists('previous_balance-exclude_from_total');
3335 my $amount = $self->charged +
3336 ( $conf->exists('disable_previous_balance', $agentnum) ||
3337 $conf->exists('previous_balance-exclude_from_total')
3341 $total->{'total_item'} = &$embolden_function($self->mt($item));
3342 $total->{'total_amount'} =
3343 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3344 if ( $multisection ) {
3345 if ( $adjust_section->{'sort_weight'} ) {
3346 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3347 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3349 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3350 $other_money_char. sprintf('%.2f', $self->charged );
3353 push @total_items, $total;
3355 push @buf,['','-----------'];
3358 sprintf( '%10.2f', $amount )
3363 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3364 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3367 my $credittotal = 0;
3368 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3371 $total->{'total_item'} = &$escape_function($credit->{'description'});
3372 $credittotal += $credit->{'amount'};
3373 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3374 $adjusttotal += $credit->{'amount'};
3375 if ( $multisection ) {
3376 my $money = $old_latex ? '' : $money_char;
3377 push @detail_items, {
3378 ext_description => [],
3381 description => &$escape_function($credit->{'description'}),
3382 amount => $money. $credit->{'amount'},
3384 section => $adjust_section,
3387 push @total_items, $total;
3391 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3394 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3395 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3399 my $paymenttotal = 0;
3400 foreach my $payment ( $self->_items_payments ) {
3402 $total->{'total_item'} = &$escape_function($payment->{'description'});
3403 $paymenttotal += $payment->{'amount'};
3404 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3405 $adjusttotal += $payment->{'amount'};
3406 if ( $multisection ) {
3407 my $money = $old_latex ? '' : $money_char;
3408 push @detail_items, {
3409 ext_description => [],
3412 description => &$escape_function($payment->{'description'}),
3413 amount => $money. $payment->{'amount'},
3415 section => $adjust_section,
3418 push @total_items, $total;
3420 push @buf, [ $payment->{'description'},
3421 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3424 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3426 if ( $multisection ) {
3427 $adjust_section->{'subtotal'} = $other_money_char.
3428 sprintf('%.2f', $adjusttotal);
3429 push @sections, $adjust_section
3430 unless $adjust_section->{sort_weight};
3433 # create Balance Due message
3436 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3437 $total->{'total_amount'} =
3438 &$embolden_function(
3439 $other_money_char. sprintf('%.2f', $summarypage
3441 $self->billing_balance
3442 : $self->owed + $pr_total
3445 if ( $multisection && !$adjust_section->{sort_weight} ) {
3446 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3447 $total->{'total_amount'};
3449 push @total_items, $total;
3451 push @buf,['','-----------'];
3452 push @buf,[$self->balance_due_msg, $money_char.
3453 sprintf("%10.2f", $balance_due ) ];
3456 if ( $conf->exists('previous_balance-show_credit')
3457 and $cust_main->balance < 0 ) {
3458 my $credit_total = {
3459 'total_item' => &$embolden_function($self->credit_balance_msg),
3460 'total_amount' => &$embolden_function(
3461 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3464 if ( $multisection ) {
3465 $adjust_section->{'posttotal'} .= $newline_token .
3466 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3469 push @total_items, $credit_total;
3471 push @buf,['','-----------'];
3472 push @buf,[$self->credit_balance_msg, $money_char.
3473 sprintf("%10.2f", -$cust_main->balance ) ];
3477 if ( $multisection ) {
3478 if ($conf->exists('svc_phone_sections')) {
3480 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3481 $total->{'total_amount'} =
3482 &$embolden_function(
3483 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3485 my $last_section = pop @sections;
3486 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3487 $total->{'total_amount'};
3488 push @sections, $last_section;
3490 push @sections, @$late_sections
3494 # make a discounts-available section, even without multisection
3495 if ( $conf->exists('discount-show_available')
3496 and my @discounts_avail = $self->_items_discounts_avail ) {
3497 my $discount_section = {
3498 'description' => $self->mt('Discounts Available'),
3503 push @sections, $discount_section;
3504 push @detail_items, map { +{
3505 'ref' => '', #should this be something else?
3506 'section' => $discount_section,
3507 'description' => &$escape_function( $_->{description} ),
3508 'amount' => $money_char . &$escape_function( $_->{amount} ),
3509 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3510 } } @discounts_avail;
3513 # All sections and items are built; now fill in templates.
3514 my @includelist = ();
3515 push @includelist, 'summary' if $summarypage;
3516 foreach my $include ( @includelist ) {
3518 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3521 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3523 @inc_src = $conf->config($inc_file, $agentnum);
3527 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3529 my $convert_map = $convert_maps{$format}{$include};
3531 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3532 s/--\@\]/$delimiters{$format}[1]/g;
3535 &$convert_map( $conf->config($inc_file, $agentnum) );
3539 my $inc_tt = new Text::Template (
3541 SOURCE => [ map "$_\n", @inc_src ],
3542 DELIMITERS => $delimiters{$format},
3543 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3545 unless ( $inc_tt->compile() ) {
3546 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3547 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3551 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3553 $invoice_data{$include} =~ s/\n+$//
3554 if ($format eq 'latex');
3559 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3560 /invoice_lines\((\d*)\)/;
3561 $invoice_lines += $1 || scalar(@buf);
3564 die "no invoice_lines() functions in template?"
3565 if ( $format eq 'template' && !$wasfunc );
3567 if ($format eq 'template') {
3569 if ( $invoice_lines ) {
3570 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3571 $invoice_data{'total_pages'}++
3572 if scalar(@buf) % $invoice_lines;
3575 #setup subroutine for the template
3576 $invoice_data{invoice_lines} = sub {
3577 my $lines = shift || scalar(@buf);
3589 push @collect, split("\n",
3590 $text_template->fill_in( HASH => \%invoice_data )
3592 $invoice_data{'page'}++;
3594 map "$_\n", @collect;
3596 # this is where we actually create the invoice
3597 warn "filling in template for invoice ". $self->invnum. "\n"
3599 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3602 $text_template->fill_in(HASH => \%invoice_data);
3606 # helper routine for generating date ranges
3607 sub _prior_month30s {
3610 [ 1, 2592000 ], # 0-30 days ago
3611 [ 2592000, 5184000 ], # 30-60 days ago
3612 [ 5184000, 7776000 ], # 60-90 days ago
3613 [ 7776000, 0 ], # 90+ days ago
3616 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3617 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3622 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3624 Returns an postscript invoice, as a scalar.
3626 Options can be passed as a hashref (recommended) or as a list of time, template
3627 and then any key/value pairs for any other options.
3629 I<time> an optional value used to control the printing of overdue messages. The
3630 default is now. It isn't the date of the invoice; that's the `_date' field.
3631 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3632 L<Time::Local> and L<Date::Parse> for conversion functions.
3634 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3641 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3642 my $ps = generate_ps($file);
3644 unlink($barcodefile) if $barcodefile;
3649 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3651 Returns an PDF invoice, as a scalar.
3653 Options can be passed as a hashref (recommended) or as a list of time, template
3654 and then any key/value pairs for any other options.
3656 I<time> an optional value used to control the printing of overdue messages. The
3657 default is now. It isn't the date of the invoice; that's the `_date' field.
3658 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3659 L<Time::Local> and L<Date::Parse> for conversion functions.
3661 I<template>, if specified, is the name of a suffix for alternate invoices.
3663 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3670 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3671 my $pdf = generate_pdf($file);
3673 unlink($barcodefile) if $barcodefile;
3678 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3680 Returns an HTML invoice, as a scalar.
3682 I<time> an optional value used to control the printing of overdue messages. The
3683 default is now. It isn't the date of the invoice; that's the `_date' field.
3684 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3685 L<Time::Local> and L<Date::Parse> for conversion functions.
3687 I<template>, if specified, is the name of a suffix for alternate invoices.
3689 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3691 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3692 when emailing the invoice as part of a multipart/related MIME email.
3700 %params = %{ shift() };
3702 $params{'time'} = shift;
3703 $params{'template'} = shift;
3704 $params{'cid'} = shift;
3707 $params{'format'} = 'html';
3709 $self->print_generic( %params );
3712 # quick subroutine for print_latex
3714 # There are ten characters that LaTeX treats as special characters, which
3715 # means that they do not simply typeset themselves:
3716 # # $ % & ~ _ ^ \ { }
3718 # TeX ignores blanks following an escaped character; if you want a blank (as
3719 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3723 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3724 $value =~ s/([<>])/\$$1\$/g;
3730 encode_entities($value);
3734 sub _html_escape_nbsp {
3735 my $value = _html_escape(shift);
3736 $value =~ s/ +/ /g;
3740 #utility methods for print_*
3742 sub _translate_old_latex_format {
3743 warn "_translate_old_latex_format called\n"
3750 if ( $line =~ /^%%Detail\s*$/ ) {
3752 push @template, q![@--!,
3753 q! foreach my $_tr_line (@detail_items) {!,
3754 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3755 q! $_tr_line->{'description'} .= !,
3756 q! "\\tabularnewline\n~~".!,
3757 q! join( "\\tabularnewline\n~~",!,
3758 q! @{$_tr_line->{'ext_description'}}!,
3762 while ( ( my $line_item_line = shift )
3763 !~ /^%%EndDetail\s*$/ ) {
3764 $line_item_line =~ s/'/\\'/g; # nice LTS
3765 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3766 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3767 push @template, " \$OUT .= '$line_item_line';";
3770 push @template, '}',
3773 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3775 push @template, '[@--',
3776 ' foreach my $_tr_line (@total_items) {';
3778 while ( ( my $total_item_line = shift )
3779 !~ /^%%EndTotalDetails\s*$/ ) {
3780 $total_item_line =~ s/'/\\'/g; # nice LTS
3781 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3782 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3783 push @template, " \$OUT .= '$total_item_line';";
3786 push @template, '}',
3790 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3791 push @template, $line;
3797 warn "$_\n" foreach @template;
3805 my $conf = $self->conf;
3807 #check for an invoice-specific override
3808 return $self->invoice_terms if $self->invoice_terms;
3810 #check for a customer- specific override
3811 my $cust_main = $self->cust_main;
3812 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3814 #use configured default
3815 $conf->config('invoice_default_terms') || '';
3821 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3822 $duedate = $self->_date() + ( $1 * 86400 );
3829 $self->due_date ? time2str(shift, $self->due_date) : '';
3832 sub balance_due_msg {
3834 my $msg = $self->mt('Balance Due');
3835 return $msg unless $self->terms;
3836 if ( $self->due_date ) {
3837 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3838 $self->due_date2str($date_format);
3839 } elsif ( $self->terms ) {
3840 $msg .= ' - '. $self->terms;
3845 sub balance_due_date {
3847 my $conf = $self->conf;
3849 if ( $conf->exists('invoice_default_terms')
3850 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3851 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3856 sub credit_balance_msg {
3858 $self->mt('Credit Balance Remaining')
3861 =item invnum_date_pretty
3863 Returns a string with the invoice number and date, for example:
3864 "Invoice #54 (3/20/2008)"
3868 sub invnum_date_pretty {
3870 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3875 Returns a string with the date, for example: "3/20/2008"
3881 time2str($date_format, $self->_date);
3884 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3886 Generate section information for all items appearing on this invoice.
3887 This will only be called for multi-section invoices.
3889 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3890 related display records (L<FS::cust_bill_pkg_display>) and organize
3891 them into two groups ("early" and "late" according to whether they come
3892 before or after the total), then into sections. A subtotal is calculated
3895 Section descriptions are returned in sort weight order. Each consists
3896 of a hash containing:
3898 description: the package category name, escaped
3899 subtotal: the total charges in that section
3900 tax_section: a flag indicating that the section contains only tax charges
3901 summarized: same as tax_section, for some reason
3902 sort_weight: the package category's sort weight
3904 If 'condense' is set on the display record, it also contains everything
3905 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3906 coderefs to generate parts of the invoice. This is not advised.
3910 LATE: an arrayref to push the "late" section hashes onto. The "early"
3911 group is simply returned from the method.
3913 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3914 Turning this on has the following effects:
3915 - Ignores display items with the 'summary' flag.
3916 - Combines all items into the "early" group.
3917 - Creates sections for all non-disabled package categories, even if they
3918 have no charges on this invoice, as well as a section with no name.
3920 ESCAPE: an escape function to use for section titles.
3922 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3923 sorted list. If there are any of these, section subtotals exclude
3926 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3927 passed through to C<_condense_section()>.
3931 use vars qw(%pkg_category_cache);
3932 sub _items_sections {
3935 my $summarypage = shift;
3937 my $extra_sections = shift;
3941 my %late_subtotal = ();
3944 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3947 my $usage = $cust_bill_pkg->usage;
3949 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3950 next if ( $display->summary && $summarypage );
3952 my $section = $display->section;
3953 my $type = $display->type;
3955 $not_tax{$section} = 1
3956 unless $cust_bill_pkg->pkgnum == 0;
3958 if ( $display->post_total && !$summarypage ) {
3959 if (! $type || $type eq 'S') {
3960 $late_subtotal{$section} += $cust_bill_pkg->setup
3961 if $cust_bill_pkg->setup != 0
3962 || $cust_bill_pkg->setup_show_zero;
3966 $late_subtotal{$section} += $cust_bill_pkg->recur
3967 if $cust_bill_pkg->recur != 0
3968 || $cust_bill_pkg->recur_show_zero;
3971 if ($type && $type eq 'R') {
3972 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3973 if $cust_bill_pkg->recur != 0
3974 || $cust_bill_pkg->recur_show_zero;
3977 if ($type && $type eq 'U') {
3978 $late_subtotal{$section} += $usage
3979 unless scalar(@$extra_sections);
3984 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3986 if (! $type || $type eq 'S') {
3987 $subtotal{$section} += $cust_bill_pkg->setup
3988 if $cust_bill_pkg->setup != 0
3989 || $cust_bill_pkg->setup_show_zero;
3993 $subtotal{$section} += $cust_bill_pkg->recur
3994 if $cust_bill_pkg->recur != 0
3995 || $cust_bill_pkg->recur_show_zero;
3998 if ($type && $type eq 'R') {
3999 $subtotal{$section} += $cust_bill_pkg->recur - $usage
4000 if $cust_bill_pkg->recur != 0
4001 || $cust_bill_pkg->recur_show_zero;
4004 if ($type && $type eq 'U') {
4005 $subtotal{$section} += $usage
4006 unless scalar(@$extra_sections);
4015 %pkg_category_cache = ();
4017 push @$late, map { { 'description' => &{$escape}($_),
4018 'subtotal' => $late_subtotal{$_},
4020 'sort_weight' => ( _pkg_category($_)
4021 ? _pkg_category($_)->weight
4024 ((_pkg_category($_) && _pkg_category($_)->condense)
4025 ? $self->_condense_section($format)
4029 sort _sectionsort keys %late_subtotal;
4032 if ( $summarypage ) {
4033 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4034 map { $_->categoryname } qsearch('pkg_category', {});
4035 push @sections, '' if exists($subtotal{''});
4037 @sections = keys %subtotal;
4040 my @early = map { { 'description' => &{$escape}($_),
4041 'subtotal' => $subtotal{$_},
4042 'summarized' => $not_tax{$_} ? '' : 'Y',
4043 'tax_section' => $not_tax{$_} ? '' : 'Y',
4044 'sort_weight' => ( _pkg_category($_)
4045 ? _pkg_category($_)->weight
4048 ((_pkg_category($_) && _pkg_category($_)->condense)
4049 ? $self->_condense_section($format)
4054 push @early, @$extra_sections if $extra_sections;
4056 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4060 #helper subs for above
4063 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4067 my $categoryname = shift;
4068 $pkg_category_cache{$categoryname} ||=
4069 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4072 my %condensed_format = (
4073 'label' => [ qw( Description Qty Amount ) ],
4075 sub { shift->{description} },
4076 sub { shift->{quantity} },
4077 sub { my($href, %opt) = @_;
4078 ($opt{dollar} || ''). $href->{amount};
4081 'align' => [ qw( l r r ) ],
4082 'span' => [ qw( 5 1 1 ) ], # unitprices?
4083 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4086 sub _condense_section {
4087 my ( $self, $format ) = ( shift, shift );
4089 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4090 qw( description_generator
4093 total_line_generator
4098 sub _condensed_generator_defaults {
4099 my ( $self, $format ) = ( shift, shift );
4100 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4109 sub _condensed_header_generator {
4110 my ( $self, $format ) = ( shift, shift );
4112 my ( $f, $prefix, $suffix, $separator, $column ) =
4113 _condensed_generator_defaults($format);
4115 if ($format eq 'latex') {
4116 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4117 $suffix = "\\\\\n\\hline";
4120 sub { my ($d,$a,$s,$w) = @_;
4121 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4123 } elsif ( $format eq 'html' ) {
4124 $prefix = '<th></th>';
4128 sub { my ($d,$a,$s,$w) = @_;
4129 return qq!<th align="$html_align{$a}">$d</th>!;
4137 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4139 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4142 $prefix. join($separator, @result). $suffix;
4147 sub _condensed_description_generator {
4148 my ( $self, $format ) = ( shift, shift );
4150 my ( $f, $prefix, $suffix, $separator, $column ) =
4151 _condensed_generator_defaults($format);
4153 my $money_char = '$';
4154 if ($format eq 'latex') {
4155 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4157 $separator = " & \n";
4159 sub { my ($d,$a,$s,$w) = @_;
4160 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4162 $money_char = '\\dollar';
4163 }elsif ( $format eq 'html' ) {
4164 $prefix = '"><td align="center"></td>';
4168 sub { my ($d,$a,$s,$w) = @_;
4169 return qq!<td align="$html_align{$a}">$d</td>!;
4171 #$money_char = $conf->config('money_char') || '$';
4172 $money_char = ''; # this is madness
4180 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4182 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4184 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4185 map { $f->{$_}->[$i] } qw(align span width)
4189 $prefix. join( $separator, @result ). $suffix;
4194 sub _condensed_total_generator {
4195 my ( $self, $format ) = ( shift, shift );
4197 my ( $f, $prefix, $suffix, $separator, $column ) =
4198 _condensed_generator_defaults($format);
4201 if ($format eq 'latex') {
4204 $separator = " & \n";
4206 sub { my ($d,$a,$s,$w) = @_;
4207 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4209 }elsif ( $format eq 'html' ) {
4213 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4215 sub { my ($d,$a,$s,$w) = @_;
4216 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4225 # my $r = &{$f->{fields}->[$i]}(@args);
4226 # $r .= ' Total' unless $i;
4228 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4230 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4231 map { $f->{$_}->[$i] } qw(align span width)
4235 $prefix. join( $separator, @result ). $suffix;
4240 =item total_line_generator FORMAT
4242 Returns a coderef used for generation of invoice total line items for this
4243 usage_class. FORMAT is either html or latex
4247 # should not be used: will have issues with hash element names (description vs
4248 # total_item and amount vs total_amount -- another array of functions?
4250 sub _condensed_total_line_generator {
4251 my ( $self, $format ) = ( shift, shift );
4253 my ( $f, $prefix, $suffix, $separator, $column ) =
4254 _condensed_generator_defaults($format);
4257 if ($format eq 'latex') {
4260 $separator = " & \n";
4262 sub { my ($d,$a,$s,$w) = @_;
4263 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4265 }elsif ( $format eq 'html' ) {
4269 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4271 sub { my ($d,$a,$s,$w) = @_;
4272 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4281 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4283 &{$column}( &{$f->{fields}->[$i]}(@args),
4284 map { $f->{$_}->[$i] } qw(align span width)
4288 $prefix. join( $separator, @result ). $suffix;
4293 #sub _items_extra_usage_sections {
4295 # my $escape = shift;
4297 # my %sections = ();
4299 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4300 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4302 # next unless $cust_bill_pkg->pkgnum > 0;
4304 # foreach my $section ( keys %usage_class ) {
4306 # my $usage = $cust_bill_pkg->usage($section);
4308 # next unless $usage && $usage > 0;
4310 # $sections{$section} ||= 0;
4311 # $sections{$section} += $usage;
4317 # map { { 'description' => &{$escape}($_),
4318 # 'subtotal' => $sections{$_},
4319 # 'summarized' => '',
4320 # 'tax_section' => '',
4323 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4327 sub _items_extra_usage_sections {
4329 my $conf = $self->conf;
4337 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4339 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4340 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4341 next unless $cust_bill_pkg->pkgnum > 0;
4343 foreach my $classnum ( keys %usage_class ) {
4344 my $section = $usage_class{$classnum}->classname;
4345 $classnums{$section} = $classnum;
4347 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4348 my $amount = $detail->amount;
4349 next unless $amount && $amount > 0;
4351 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4352 $sections{$section}{amount} += $amount; #subtotal
4353 $sections{$section}{calls}++;
4354 $sections{$section}{duration} += $detail->duration;
4356 my $desc = $detail->regionname;
4357 my $description = $desc;
4358 $description = substr($desc, 0, $maxlength). '...'
4359 if $format eq 'latex' && length($desc) > $maxlength;
4361 $lines{$section}{$desc} ||= {
4362 description => &{$escape}($description),
4363 #pkgpart => $part_pkg->pkgpart,
4364 pkgnum => $cust_bill_pkg->pkgnum,
4369 #unit_amount => $cust_bill_pkg->unitrecur,
4370 quantity => $cust_bill_pkg->quantity,
4371 product_code => 'N/A',
4372 ext_description => [],
4375 $lines{$section}{$desc}{amount} += $amount;
4376 $lines{$section}{$desc}{calls}++;
4377 $lines{$section}{$desc}{duration} += $detail->duration;
4383 my %sectionmap = ();
4384 foreach (keys %sections) {
4385 my $usage_class = $usage_class{$classnums{$_}};
4386 $sectionmap{$_} = { 'description' => &{$escape}($_),
4387 'amount' => $sections{$_}{amount}, #subtotal
4388 'calls' => $sections{$_}{calls},
4389 'duration' => $sections{$_}{duration},
4391 'tax_section' => '',
4392 'sort_weight' => $usage_class->weight,
4393 ( $usage_class->format
4394 ? ( map { $_ => $usage_class->$_($format) }
4395 qw( description_generator header_generator total_generator total_line_generator )
4402 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4406 foreach my $section ( keys %lines ) {
4407 foreach my $line ( keys %{$lines{$section}} ) {
4408 my $l = $lines{$section}{$line};
4409 $l->{section} = $sectionmap{$section};
4410 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4411 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4416 return(\@sections, \@lines);
4422 my $end = $self->_date;
4424 # start at date of previous invoice + 1 second or 0 if no previous invoice
4425 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4426 $start = 0 if !$start;
4429 my $cust_main = $self->cust_main;
4430 my @pkgs = $cust_main->all_pkgs;
4431 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4434 foreach my $pkg ( @pkgs ) {
4435 my @h_cust_svc = $pkg->h_cust_svc($end);
4436 foreach my $h_cust_svc ( @h_cust_svc ) {
4437 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4438 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4440 my $inserted = $h_cust_svc->date_inserted;
4441 my $deleted = $h_cust_svc->date_deleted;
4442 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4444 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4446 # DID either activated or ported in; cannot be both for same DID simultaneously
4447 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4448 && (!$phone_inserted->lnp_status
4449 || $phone_inserted->lnp_status eq ''
4450 || $phone_inserted->lnp_status eq 'native')) {
4453 else { # this one not so clean, should probably move to (h_)svc_phone
4454 my $phone_portedin = qsearchs( 'h_svc_phone',
4455 { 'svcnum' => $h_cust_svc->svcnum,
4456 'lnp_status' => 'portedin' },
4457 FS::h_svc_phone->sql_h_searchs($end),
4459 $num_portedin++ if $phone_portedin;
4462 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4463 if($deleted >= $start && $deleted <= $end && $phone_deleted
4464 && (!$phone_deleted->lnp_status
4465 || $phone_deleted->lnp_status ne 'portingout')) {
4468 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4469 && $phone_deleted->lnp_status
4470 && $phone_deleted->lnp_status eq 'portingout') {
4474 # increment usage minutes
4475 if ( $phone_inserted ) {
4476 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4477 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4480 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4483 # don't look at this service again
4484 push @seen, $h_cust_svc->svcnum;
4488 $minutes = sprintf("%d", $minutes);
4489 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4490 . "$num_deactivated Ported-Out: $num_portedout ",
4491 "Total Minutes: $minutes");
4494 sub _items_accountcode_cdr {
4499 my $section = { 'amount' => 0,
4502 'sort_weight' => '',
4504 'description' => 'Usage by Account Code',
4510 my %accountcodes = ();
4512 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4513 next unless $cust_bill_pkg->pkgnum > 0;
4515 my @header = $cust_bill_pkg->details_header;
4516 next unless scalar(@header);
4517 $section->{'header'} = join(',',@header);
4519 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4521 $section->{'header'} = $detail->formatted('format' => $format)
4522 if($detail->detail eq $section->{'header'});
4524 my $accountcode = $detail->accountcode;
4525 next unless $accountcode;
4527 my $amount = $detail->amount;
4528 next unless $amount && $amount > 0;
4530 $accountcodes{$accountcode} ||= {
4531 description => $accountcode,
4538 product_code => 'N/A',
4539 section => $section,
4540 ext_description => [ $section->{'header'} ],
4544 $section->{'amount'} += $amount;
4545 $accountcodes{$accountcode}{'amount'} += $amount;
4546 $accountcodes{$accountcode}{calls}++;
4547 $accountcodes{$accountcode}{duration} += $detail->duration;
4548 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4552 foreach my $l ( values %accountcodes ) {
4553 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4554 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4555 foreach my $sorted_detail ( @sorted_detail ) {
4556 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4558 delete $l->{detail_temp};
4562 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4564 return ($section,\@sorted_lines);
4567 sub _items_svc_phone_sections {
4569 my $conf = $self->conf;
4577 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4579 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4580 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4582 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4583 next unless $cust_bill_pkg->pkgnum > 0;
4585 my @header = $cust_bill_pkg->details_header;
4586 next unless scalar(@header);
4588 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4590 my $phonenum = $detail->phonenum;
4591 next unless $phonenum;
4593 my $amount = $detail->amount;
4594 next unless $amount && $amount > 0;
4596 $sections{$phonenum} ||= { 'amount' => 0,
4599 'sort_weight' => -1,
4600 'phonenum' => $phonenum,
4602 $sections{$phonenum}{amount} += $amount; #subtotal
4603 $sections{$phonenum}{calls}++;
4604 $sections{$phonenum}{duration} += $detail->duration;
4606 my $desc = $detail->regionname;
4607 my $description = $desc;
4608 $description = substr($desc, 0, $maxlength). '...'
4609 if $format eq 'latex' && length($desc) > $maxlength;
4611 $lines{$phonenum}{$desc} ||= {
4612 description => &{$escape}($description),
4613 #pkgpart => $part_pkg->pkgpart,
4621 product_code => 'N/A',
4622 ext_description => [],
4625 $lines{$phonenum}{$desc}{amount} += $amount;
4626 $lines{$phonenum}{$desc}{calls}++;
4627 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4629 my $line = $usage_class{$detail->classnum}->classname;
4630 $sections{"$phonenum $line"} ||=
4634 'sort_weight' => $usage_class{$detail->classnum}->weight,
4635 'phonenum' => $phonenum,
4636 'header' => [ @header ],
4638 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4639 $sections{"$phonenum $line"}{calls}++;
4640 $sections{"$phonenum $line"}{duration} += $detail->duration;
4642 $lines{"$phonenum $line"}{$desc} ||= {
4643 description => &{$escape}($description),
4644 #pkgpart => $part_pkg->pkgpart,
4652 product_code => 'N/A',
4653 ext_description => [],
4656 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4657 $lines{"$phonenum $line"}{$desc}{calls}++;
4658 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4659 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4660 $detail->formatted('format' => $format);
4665 my %sectionmap = ();
4666 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4667 foreach ( keys %sections ) {
4668 my @header = @{ $sections{$_}{header} || [] };
4670 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4671 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4672 my $usage_class = $summary ? $simple : $usage_simple;
4673 my $ending = $summary ? ' usage charges' : '';
4676 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4678 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4679 'amount' => $sections{$_}{amount}, #subtotal
4680 'calls' => $sections{$_}{calls},
4681 'duration' => $sections{$_}{duration},
4683 'tax_section' => '',
4684 'phonenum' => $sections{$_}{phonenum},
4685 'sort_weight' => $sections{$_}{sort_weight},
4686 'post_total' => $summary, #inspire pagebreak
4688 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4689 qw( description_generator
4692 total_line_generator
4699 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4700 $a->{sort_weight} <=> $b->{sort_weight}
4705 foreach my $section ( keys %lines ) {
4706 foreach my $line ( keys %{$lines{$section}} ) {
4707 my $l = $lines{$section}{$line};
4708 $l->{section} = $sectionmap{$section};
4709 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4710 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4715 if($conf->exists('phone_usage_class_summary')) {
4716 # this only works with Latex
4720 # after this, we'll have only two sections per DID:
4721 # Calls Summary and Calls Detail
4722 foreach my $section ( @sections ) {
4723 if($section->{'post_total'}) {
4724 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4725 $section->{'total_line_generator'} = sub { '' };
4726 $section->{'total_generator'} = sub { '' };
4727 $section->{'header_generator'} = sub { '' };
4728 $section->{'description_generator'} = '';
4729 push @newsections, $section;
4730 my %calls_detail = %$section;
4731 $calls_detail{'post_total'} = '';
4732 $calls_detail{'sort_weight'} = '';
4733 $calls_detail{'description_generator'} = sub { '' };
4734 $calls_detail{'header_generator'} = sub {
4735 return ' & Date/Time & Called Number & Duration & Price'
4736 if $format eq 'latex';
4739 $calls_detail{'description'} = 'Calls Detail: '
4740 . $section->{'phonenum'};
4741 push @newsections, \%calls_detail;
4745 # after this, each usage class is collapsed/summarized into a single
4746 # line under the Calls Summary section
4747 foreach my $newsection ( @newsections ) {
4748 if($newsection->{'post_total'}) { # this means Calls Summary
4749 foreach my $section ( @sections ) {
4750 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4751 && !$section->{'post_total'});
4752 my $newdesc = $section->{'description'};
4753 my $tn = $section->{'phonenum'};
4754 $newdesc =~ s/$tn//g;
4755 my $line = { ext_description => [],
4759 calls => $section->{'calls'},
4760 section => $newsection,
4761 duration => $section->{'duration'},
4762 description => $newdesc,
4763 amount => sprintf("%.2f",$section->{'amount'}),
4764 product_code => 'N/A',
4766 push @newlines, $line;
4771 # after this, Calls Details is populated with all CDRs
4772 foreach my $newsection ( @newsections ) {
4773 if(!$newsection->{'post_total'}) { # this means Calls Details
4774 foreach my $line ( @lines ) {
4775 next unless (scalar(@{$line->{'ext_description'}}) &&
4776 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4778 my @extdesc = @{$line->{'ext_description'}};
4780 foreach my $extdesc ( @extdesc ) {
4781 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4782 push @newextdesc, $extdesc;
4784 $line->{'ext_description'} = \@newextdesc;
4785 $line->{'section'} = $newsection;
4786 push @newlines, $line;
4791 return(\@newsections, \@newlines);
4794 return(\@sections, \@lines);
4798 sub _items { # seems to be unused
4801 #my @display = scalar(@_)
4803 # : qw( _items_previous _items_pkg );
4804 # #: qw( _items_pkg );
4805 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4806 my @display = qw( _items_previous _items_pkg );
4809 foreach my $display ( @display ) {
4810 push @b, $self->$display(@_);
4815 sub _items_previous {
4817 my $conf = $self->conf;
4818 my $cust_main = $self->cust_main;
4819 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4821 foreach ( @pr_cust_bill ) {
4822 my $date = $conf->exists('invoice_show_prior_due_date')
4823 ? 'due '. $_->due_date2str($date_format)
4824 : time2str($date_format, $_->_date);
4826 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4827 #'pkgpart' => 'N/A',
4829 'amount' => sprintf("%.2f", $_->owed),
4835 # 'description' => 'Previous Balance',
4836 # #'pkgpart' => 'N/A',
4837 # 'pkgnum' => 'N/A',
4838 # 'amount' => sprintf("%10.2f", $pr_total ),
4839 # 'ext_description' => [ map {
4840 # "Invoice ". $_->invnum.
4841 # " (". time2str("%x",$_->_date). ") ".
4842 # sprintf("%10.2f", $_->owed)
4843 # } @pr_cust_bill ],
4848 =item _items_pkg [ OPTIONS ]
4850 Return line item hashes for each package item on this invoice. Nearly
4853 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4855 The only OPTIONS accepted is 'section', which may point to a hashref
4856 with a key named 'condensed', which may have a true value. If it
4857 does, this method tries to merge identical items into items with
4858 'quantity' equal to the number of items (not the sum of their
4859 separate quantities, for some reason).
4867 warn "$me _items_pkg searching for all package line items\n"
4870 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4872 warn "$me _items_pkg filtering line items\n"
4874 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4876 if ($options{section} && $options{section}->{condensed}) {
4878 warn "$me _items_pkg condensing section\n"
4882 local $Storable::canonical = 1;
4883 foreach ( @items ) {
4885 delete $item->{ref};
4886 delete $item->{ext_description};
4887 my $key = freeze($item);
4888 $itemshash{$key} ||= 0;
4889 $itemshash{$key} ++; # += $item->{quantity};
4891 @items = sort { $a->{description} cmp $b->{description} }
4892 map { my $i = thaw($_);
4893 $i->{quantity} = $itemshash{$_};
4895 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4901 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4908 return 0 unless $a->itemdesc cmp $b->itemdesc;
4909 return -1 if $b->itemdesc eq 'Tax';
4910 return 1 if $a->itemdesc eq 'Tax';
4911 return -1 if $b->itemdesc eq 'Other surcharges';
4912 return 1 if $a->itemdesc eq 'Other surcharges';
4913 $a->itemdesc cmp $b->itemdesc;
4918 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4919 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4922 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4924 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4925 list of hashrefs describing the line items they generate on the invoice.
4927 OPTIONS may include:
4929 format: the invoice format.
4931 escape_function: the function used to escape strings.
4933 DEPRECATED? (expensive, mostly unused?)
4934 format_function: the function used to format CDRs.
4936 section: a hashref containing 'description'; if this is present,
4937 cust_bill_pkg_display records not belonging to this section are
4940 multisection: a flag indicating that this is a multisection invoice,
4941 which does something complicated.
4943 multilocation: a flag to display the location label for the package.
4945 Returns a list of hashrefs, each of which may contain:
4947 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4948 ext_description, which is an arrayref of detail lines to show below
4953 sub _items_cust_bill_pkg {
4955 my $conf = $self->conf;
4956 my $cust_bill_pkgs = shift;
4959 my $format = $opt{format} || '';
4960 my $escape_function = $opt{escape_function} || sub { shift };
4961 my $format_function = $opt{format_function} || '';
4962 my $no_usage = $opt{no_usage} || '';
4963 my $unsquelched = $opt{unsquelched} || ''; #unused
4964 my $section = $opt{section}->{description} if $opt{section};
4965 my $summary_page = $opt{summary_page} || ''; #unused
4966 my $multilocation = $opt{multilocation} || '';
4967 my $multisection = $opt{multisection} || '';
4968 my $discount_show_always = 0;
4970 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4972 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4975 my ($s, $r, $u) = ( undef, undef, undef );
4976 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4979 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4980 if ( $_ && !$cust_bill_pkg->hidden ) {
4981 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4982 $_->{amount} =~ s/^\-0\.00$/0.00/;
4983 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4985 if $_->{amount} != 0
4986 || $discount_show_always
4987 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4988 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4994 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4996 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4997 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
5000 foreach my $display ( grep { defined($section)
5001 ? $_->section eq $section
5004 #grep { !$_->summary || !$summary_page } # bunk!
5005 grep { !$_->summary || $multisection }
5006 @cust_bill_pkg_display
5010 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5011 $display->billpkgdisplaynum. "\n"
5014 my $type = $display->type;
5016 my $desc = $cust_bill_pkg->desc;
5017 $desc = substr($desc, 0, $maxlength). '...'
5018 if $format eq 'latex' && length($desc) > $maxlength;
5020 my %details_opt = ( 'format' => $format,
5021 'escape_function' => $escape_function,
5022 'format_function' => $format_function,
5023 'no_usage' => $opt{'no_usage'},
5026 if ( $cust_bill_pkg->pkgnum > 0 ) {
5028 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5031 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5033 # start/end dates for invoice formats that do nonstandard
5035 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
5037 if ( (!$type || $type eq 'S')
5038 && ( $cust_bill_pkg->setup != 0
5039 || $cust_bill_pkg->setup_show_zero
5044 warn "$me _items_cust_bill_pkg adding setup\n"
5047 my $description = $desc;
5048 $description .= ' Setup'
5049 if $cust_bill_pkg->recur != 0
5050 || $discount_show_always
5051 || $cust_bill_pkg->recur_show_zero;
5054 unless ( $cust_pkg->part_pkg->hide_svc_detail
5055 || $cust_bill_pkg->hidden )
5058 push @d, map &{$escape_function}($_),
5059 $cust_pkg->h_labels_short($self->_date, undef, 'I')
5060 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5062 if ( $multilocation ) {
5063 my $loc = $cust_pkg->location_label;
5064 $loc = substr($loc, 0, $maxlength). '...'
5065 if $format eq 'latex' && length($loc) > $maxlength;
5066 push @d, &{$escape_function}($loc);
5069 } #unless hiding service details
5071 push @d, $cust_bill_pkg->details(%details_opt)
5072 if $cust_bill_pkg->recur == 0;
5074 if ( $cust_bill_pkg->hidden ) {
5075 $s->{amount} += $cust_bill_pkg->setup;
5076 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5077 push @{ $s->{ext_description} }, @d;
5081 description => $description,
5082 #pkgpart => $part_pkg->pkgpart,
5083 pkgnum => $cust_bill_pkg->pkgnum,
5084 amount => $cust_bill_pkg->setup,
5085 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5086 unit_amount => $cust_bill_pkg->unitsetup,
5087 quantity => $cust_bill_pkg->quantity,
5088 ext_description => \@d,
5094 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5096 $cust_bill_pkg->recur != 0
5097 || $cust_bill_pkg->setup == 0
5098 || $discount_show_always
5099 || $cust_bill_pkg->recur_show_zero
5104 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5107 my $is_summary = $display->summary;
5108 my $description = ($is_summary && $type && $type eq 'U')
5109 ? "Usage charges" : $desc;
5111 #pry be a bit more efficient to look some of this conf stuff up
5114 $conf->exists('disable_line_item_date_ranges')
5115 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5118 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5119 $cust_main->agentnum
5121 if ( defined($date_style) && $date_style eq 'month_of' ) {
5122 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5123 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5124 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5125 $cust_main->agentnum
5127 $desc .= ' ' unless $desc =~ /\s$/;
5128 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5130 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5131 " - ". time2str($date_format, $cust_bill_pkg->edate);
5133 $description .= " ($time_period)";
5137 my @seconds = (); # for display of usage info
5139 #at least until cust_bill_pkg has "past" ranges in addition to
5140 #the "future" sdate/edate ones... see #3032
5141 my @dates = ( $self->_date );
5142 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5143 push @dates, $prev->sdate if $prev;
5144 push @dates, undef if !$prev;
5146 unless ( $cust_pkg->part_pkg->hide_svc_detail
5147 || $cust_bill_pkg->itemdesc
5148 || $cust_bill_pkg->hidden
5149 || $is_summary && $type && $type eq 'U' )
5152 warn "$me _items_cust_bill_pkg adding service details\n"
5155 push @d, map &{$escape_function}($_),
5156 $cust_pkg->h_labels_short(@dates, 'I')
5157 #$cust_bill_pkg->edate,
5158 #$cust_bill_pkg->sdate)
5159 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5161 warn "$me _items_cust_bill_pkg done adding service details\n"
5164 if ( $multilocation ) {
5165 my $loc = $cust_pkg->location_label;
5166 $loc = substr($loc, 0, $maxlength). '...'
5167 if $format eq 'latex' && length($loc) > $maxlength;
5168 push @d, &{$escape_function}($loc);
5171 # Display of seconds_since_sqlradacct:
5172 # On the invoice, when processing @detail_items, look for a field
5173 # named 'seconds'. This will contain total seconds for each
5174 # service, in the same order as @ext_description. For services
5175 # that don't support this it will show undef.
5176 if ( $conf->exists('svc_acct-usage_seconds')
5177 and ! $cust_bill_pkg->pkgpart_override ) {
5178 foreach my $cust_svc (
5179 $cust_pkg->h_cust_svc(@dates, 'I')
5182 # eval because not having any part_export_usage exports
5183 # is a fatal error, last_bill/_date because that's how
5184 # sqlradius_hour billing does it
5186 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5188 push @seconds, $sec;
5190 } #if svc_acct-usage_seconds
5194 unless ( $is_summary ) {
5195 warn "$me _items_cust_bill_pkg adding details\n"
5198 #instead of omitting details entirely in this case (unwanted side
5199 # effects), just omit CDRs
5200 $details_opt{'no_usage'} = 1
5201 if $type && $type eq 'R';
5203 push @d, $cust_bill_pkg->details(%details_opt);
5206 warn "$me _items_cust_bill_pkg calculating amount\n"
5211 $amount = $cust_bill_pkg->recur;
5212 } elsif ($type eq 'R') {
5213 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5214 } elsif ($type eq 'U') {
5215 $amount = $cust_bill_pkg->usage;
5218 if ( !$type || $type eq 'R' ) {
5220 warn "$me _items_cust_bill_pkg adding recur\n"
5223 if ( $cust_bill_pkg->hidden ) {
5224 $r->{amount} += $amount;
5225 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5226 push @{ $r->{ext_description} }, @d;
5229 description => $description,
5230 #pkgpart => $part_pkg->pkgpart,
5231 pkgnum => $cust_bill_pkg->pkgnum,
5233 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5234 unit_amount => $cust_bill_pkg->unitrecur,
5235 quantity => $cust_bill_pkg->quantity,
5237 ext_description => \@d,
5239 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5242 } else { # $type eq 'U'
5244 warn "$me _items_cust_bill_pkg adding usage\n"
5247 if ( $cust_bill_pkg->hidden ) {
5248 $u->{amount} += $amount;
5249 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5250 push @{ $u->{ext_description} }, @d;
5253 description => $description,
5254 #pkgpart => $part_pkg->pkgpart,
5255 pkgnum => $cust_bill_pkg->pkgnum,
5257 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5258 unit_amount => $cust_bill_pkg->unitrecur,
5259 quantity => $cust_bill_pkg->quantity,
5261 ext_description => \@d,
5266 } # recurring or usage with recurring charge
5268 } else { #pkgnum tax or one-shot line item (??)
5270 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5273 if ( $cust_bill_pkg->setup != 0 ) {
5275 'description' => $desc,
5276 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5279 if ( $cust_bill_pkg->recur != 0 ) {
5281 'description' => "$desc (".
5282 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5283 time2str($date_format, $cust_bill_pkg->edate). ')',
5284 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5292 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5293 && $conf->exists('discount-show-always'));
5297 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5299 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5300 $_->{amount} =~ s/^\-0\.00$/0.00/;
5301 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5303 if $_->{amount} != 0
5304 || $discount_show_always
5305 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5306 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5310 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5317 sub _items_credits {
5318 my( $self, %opt ) = @_;
5319 my $trim_len = $opt{'trim_len'} || 60;
5323 foreach ( $self->cust_credited ) {
5325 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5327 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5328 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5329 $reason = " ($reason) " if $reason;
5332 #'description' => 'Credit ref\#'. $_->crednum.
5333 # " (". time2str("%x",$_->cust_credit->_date) .")".
5335 'description' => $self->mt('Credit applied').' '.
5336 time2str($date_format,$_->cust_credit->_date). $reason,
5337 'amount' => sprintf("%.2f",$_->amount),
5345 sub _items_payments {
5349 #get & print payments
5350 foreach ( $self->cust_bill_pay ) {
5352 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5355 'description' => $self->mt('Payment received').' '.
5356 time2str($date_format,$_->cust_pay->_date ),
5357 'amount' => sprintf("%.2f", $_->amount )
5365 =item _items_discounts_avail
5367 Returns an array of line item hashrefs representing available term discounts
5368 for this invoice. This makes the same assumptions that apply to term
5369 discounts in general: that the package is billed monthly, at a flat rate,
5370 with no usage charges. A prorated first month will be handled, as will
5371 a setup fee if the discount is allowed to apply to setup fees.
5375 sub _items_discounts_avail {
5377 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5379 my %plans = $self->discount_plans;
5381 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5385 my $plan = $plans{$months};
5387 my $term_total = sprintf('%.2f', $plan->discounted_total);
5388 my $percent = sprintf('%.0f',
5389 100 * (1 - $term_total / $plan->base_total) );
5390 my $permonth = sprintf('%.2f', $term_total / $months);
5391 my $detail = $self->mt('discount on item'). ' '.
5392 join(', ', map { "#$_" } $plan->pkgnums)
5395 # discounts for non-integer months don't work anyway
5396 $months = sprintf("%d", $months);
5399 description => $self->mt('Save [_1]% by paying for [_2] months',
5401 amount => $self->mt('[_1] ([_2] per month)',
5402 $term_total, $money_char.$permonth),
5403 ext_description => ($detail || ''),
5406 sort { $b <=> $a } keys %plans;
5410 =item call_details [ OPTION => VALUE ... ]
5412 Returns an array of CSV strings representing the call details for this invoice
5413 The only option available is the boolean prepend_billed_number
5418 my ($self, %opt) = @_;
5420 my $format_function = sub { shift };
5422 if ($opt{prepend_billed_number}) {
5423 $format_function = sub {
5427 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5432 my @details = map { $_->details( 'format_function' => $format_function,
5433 'escape_function' => sub{ return() },
5437 $self->cust_bill_pkg;
5438 my $header = $details[0];
5439 ( $header, grep { $_ ne $header } @details );
5449 =item process_reprint
5453 sub process_reprint {
5454 process_re_X('print', @_);
5457 =item process_reemail
5461 sub process_reemail {
5462 process_re_X('email', @_);
5470 process_re_X('fax', @_);
5478 process_re_X('ftp', @_);
5485 sub process_respool {
5486 process_re_X('spool', @_);
5489 use Storable qw(thaw);
5493 my( $method, $job ) = ( shift, shift );
5494 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5496 my $param = thaw(decode_base64(shift));
5497 warn Dumper($param) if $DEBUG;
5508 # spool_invoice ftp_invoice fax_invoice print_invoice
5509 my($method, $job, %param ) = @_;
5511 warn "re_X $method for job $job with param:\n".
5512 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5515 #some false laziness w/search/cust_bill.html
5517 my $orderby = 'ORDER BY cust_bill._date';
5519 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5521 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5523 my @cust_bill = qsearch( {
5524 #'select' => "cust_bill.*",
5525 'table' => 'cust_bill',
5526 'addl_from' => $addl_from,
5528 'extra_sql' => $extra_sql,
5529 'order_by' => $orderby,
5533 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5535 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5538 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5539 foreach my $cust_bill ( @cust_bill ) {
5540 $cust_bill->$method();
5542 if ( $job ) { #progressbar foo
5544 if ( time - $min_sec > $last ) {
5545 my $error = $job->update_statustext(
5546 int( 100 * $num / scalar(@cust_bill) )
5548 die $error if $error;
5559 =head1 CLASS METHODS
5565 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5570 my ($class, $start, $end) = @_;
5572 $class->paid_sql($start, $end). ' - '.
5573 $class->credited_sql($start, $end);
5578 Returns an SQL fragment to retreive the net amount (charged minus credited).
5583 my ($class, $start, $end) = @_;
5584 'charged - '. $class->credited_sql($start, $end);
5589 Returns an SQL fragment to retreive the amount paid against this invoice.
5594 my ($class, $start, $end) = @_;
5595 $start &&= "AND cust_bill_pay._date <= $start";
5596 $end &&= "AND cust_bill_pay._date > $end";
5597 $start = '' unless defined($start);
5598 $end = '' unless defined($end);
5599 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5600 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5605 Returns an SQL fragment to retreive the amount credited against this invoice.
5610 my ($class, $start, $end) = @_;
5611 $start &&= "AND cust_credit_bill._date <= $start";
5612 $end &&= "AND cust_credit_bill._date > $end";
5613 $start = '' unless defined($start);
5614 $end = '' unless defined($end);
5615 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5616 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5621 Returns an SQL fragment to retrieve the due date of an invoice.
5622 Currently only supported on PostgreSQL.
5627 my $conf = new FS::Conf;
5631 cust_bill.invoice_terms,
5632 cust_main.invoice_terms,
5633 \''.($conf->config('invoice_default_terms') || '').'\'
5634 ), E\'Net (\\\\d+)\'
5636 ) * 86400 + cust_bill._date'
5639 =item search_sql_where HASHREF
5641 Class method which returns an SQL WHERE fragment to search for parameters
5642 specified in HASHREF. Valid parameters are
5648 List reference of start date, end date, as UNIX timestamps.
5658 List reference of charged limits (exclusive).
5662 List reference of charged limits (exclusive).
5666 flag, return open invoices only
5670 flag, return net invoices only
5674 =item newest_percust
5678 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5682 sub search_sql_where {
5683 my($class, $param) = @_;
5685 warn "$me search_sql_where called with params: \n".
5686 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5692 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5693 push @search, "cust_main.agentnum = $1";
5697 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5698 push @search, "cust_bill.custnum = $1";
5702 if ( $param->{_date} ) {
5703 my($beginning, $ending) = @{$param->{_date}};
5705 push @search, "cust_bill._date >= $beginning",
5706 "cust_bill._date < $ending";
5710 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5711 push @search, "cust_bill.invnum >= $1";
5713 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5714 push @search, "cust_bill.invnum <= $1";
5718 if ( $param->{charged} ) {
5719 my @charged = ref($param->{charged})
5720 ? @{ $param->{charged} }
5721 : ($param->{charged});
5723 push @search, map { s/^charged/cust_bill.charged/; $_; }
5727 my $owed_sql = FS::cust_bill->owed_sql;
5730 if ( $param->{owed} ) {
5731 my @owed = ref($param->{owed})
5732 ? @{ $param->{owed} }
5734 push @search, map { s/^owed/$owed_sql/; $_; }
5739 push @search, "0 != $owed_sql"
5740 if $param->{'open'};
5741 push @search, '0 != '. FS::cust_bill->net_sql
5745 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5746 if $param->{'days'};
5749 if ( $param->{'newest_percust'} ) {
5751 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5752 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5754 my @newest_where = map { my $x = $_;
5755 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5758 grep ! /^cust_main./, @search;
5759 my $newest_where = scalar(@newest_where)
5760 ? ' AND '. join(' AND ', @newest_where)
5764 push @search, "cust_bill._date = (
5765 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5766 WHERE newest_cust_bill.custnum = cust_bill.custnum
5772 #promised_date - also has an option to accept nulls
5773 if ( $param->{promised_date} ) {
5774 my($beginning, $ending, $null) = @{$param->{promised_date}};
5776 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5777 "cust_bill.promised_date < $ending )" .
5778 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5781 #agent virtualization
5782 my $curuser = $FS::CurrentUser::CurrentUser;
5783 if ( $curuser->username eq 'fs_queue'
5784 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5786 my $newuser = qsearchs('access_user', {
5787 'username' => $username,
5791 $curuser = $newuser;
5793 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5796 push @search, $curuser->agentnums_sql;
5798 join(' AND ', @search );
5810 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5811 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base