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 },
394 'invnum' => { op=>'<', value=>$self->invnum },
397 foreach ( @cust_bill ) { $total += $_->owed; }
401 =item enable_previous
403 Whether to show the 'Previous Charges' section when printing this invoice.
404 The negation of the 'disable_previous_balance' config setting.
408 sub enable_previous {
410 my $agentnum = $self->cust_main->agentnum;
411 !$self->conf->exists('disable_previous_balance', $agentnum);
416 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
423 { 'table' => 'cust_bill_pkg',
424 'hashref' => { 'invnum' => $self->invnum },
425 'order_by' => 'ORDER BY billpkgnum',
430 =item cust_bill_pkg_pkgnum PKGNUM
432 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
437 sub cust_bill_pkg_pkgnum {
438 my( $self, $pkgnum ) = @_;
440 { 'table' => 'cust_bill_pkg',
441 'hashref' => { 'invnum' => $self->invnum,
444 'order_by' => 'ORDER BY billpkgnum',
451 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
458 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
459 $self->cust_bill_pkg;
461 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
466 Returns true if any of the packages (or their definitions) corresponding to the
467 line items for this invoice have the no_auto flag set.
473 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
476 =item open_cust_bill_pkg
478 Returns the open line items for this invoice.
480 Note that cust_bill_pkg with both setup and recur fees are returned as two
481 separate line items, each with only one fee.
485 # modeled after cust_main::open_cust_bill
486 sub open_cust_bill_pkg {
489 # grep { $_->owed > 0 } $self->cust_bill_pkg
491 my %other = ( 'recur' => 'setup',
492 'setup' => 'recur', );
494 foreach my $field ( qw( recur setup )) {
495 push @open, map { $_->set( $other{$field}, 0 ); $_; }
496 grep { $_->owed($field) > 0 }
497 $self->cust_bill_pkg;
503 =item cust_bill_event
505 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
509 sub cust_bill_event {
511 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
514 =item num_cust_bill_event
516 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
520 sub num_cust_bill_event {
523 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
524 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
525 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
526 $sth->fetchrow_arrayref->[0];
531 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
535 #false laziness w/cust_pkg.pm
539 'table' => 'cust_event',
540 'addl_from' => 'JOIN part_event USING ( eventpart )',
541 'hashref' => { 'tablenum' => $self->invnum },
542 'extra_sql' => " AND eventtable = 'cust_bill' ",
548 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
552 #false laziness w/cust_pkg.pm
556 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
557 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
558 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
559 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
560 $sth->fetchrow_arrayref->[0];
565 Returns the customer (see L<FS::cust_main>) for this invoice.
571 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
574 =item cust_suspend_if_balance_over AMOUNT
576 Suspends the customer associated with this invoice if the total amount owed on
577 this invoice and all older invoices is greater than the specified amount.
579 Returns a list: an empty list on success or a list of errors.
583 sub cust_suspend_if_balance_over {
584 my( $self, $amount ) = ( shift, shift );
585 my $cust_main = $self->cust_main;
586 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
589 $cust_main->suspend(@_);
595 Depreciated. See the cust_credited method.
597 #Returns a list consisting of the total previous credited (see
598 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
599 #outstanding credits (FS::cust_credit objects).
605 croak "FS::cust_bill->cust_credit depreciated; see ".
606 "FS::cust_bill->cust_credit_bill";
609 #my @cust_credit = sort { $a->_date <=> $b->_date }
610 # grep { $_->credited != 0 && $_->_date < $self->_date }
611 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
613 #foreach (@cust_credit) { $total += $_->credited; }
614 #$total, @cust_credit;
619 Depreciated. See the cust_bill_pay method.
621 #Returns all payments (see L<FS::cust_pay>) for this invoice.
627 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
629 #sort { $a->_date <=> $b->_date }
630 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
636 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
639 sub cust_bill_pay_batch {
641 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
646 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
652 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
653 sort { $a->_date <=> $b->_date }
654 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
659 =item cust_credit_bill
661 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
667 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
668 sort { $a->_date <=> $b->_date }
669 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
673 sub cust_credit_bill {
674 shift->cust_credited(@_);
677 #=item cust_bill_pay_pkgnum PKGNUM
679 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
680 #with matching pkgnum.
684 #sub cust_bill_pay_pkgnum {
685 # my( $self, $pkgnum ) = @_;
686 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
687 # sort { $a->_date <=> $b->_date }
688 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
689 # 'pkgnum' => $pkgnum,
694 =item cust_bill_pay_pkg PKGNUM
696 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
697 applied against the matching pkgnum.
701 sub cust_bill_pay_pkg {
702 my( $self, $pkgnum ) = @_;
705 'select' => 'cust_bill_pay_pkg.*',
706 'table' => 'cust_bill_pay_pkg',
707 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
708 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
709 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
710 " AND cust_bill_pkg.pkgnum = $pkgnum",
715 #=item cust_credited_pkgnum PKGNUM
717 #=item cust_credit_bill_pkgnum PKGNUM
719 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
720 #with matching pkgnum.
724 #sub cust_credited_pkgnum {
725 # my( $self, $pkgnum ) = @_;
726 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
727 # sort { $a->_date <=> $b->_date }
728 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
729 # 'pkgnum' => $pkgnum,
734 #sub cust_credit_bill_pkgnum {
735 # shift->cust_credited_pkgnum(@_);
738 =item cust_credit_bill_pkg PKGNUM
740 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
741 applied against the matching pkgnum.
745 sub cust_credit_bill_pkg {
746 my( $self, $pkgnum ) = @_;
749 'select' => 'cust_credit_bill_pkg.*',
750 'table' => 'cust_credit_bill_pkg',
751 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
752 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
753 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
754 " AND cust_bill_pkg.pkgnum = $pkgnum",
759 =item cust_bill_batch
761 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
765 sub cust_bill_batch {
767 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
772 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
773 hash keyed by term length.
779 FS::discount_plan->all($self);
784 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
791 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
793 foreach (@taxlines) { $total += $_->setup; }
799 Returns the amount owed (still outstanding) on this invoice, which is charged
800 minus all payment applications (see L<FS::cust_bill_pay>) and credit
801 applications (see L<FS::cust_credit_bill>).
807 my $balance = $self->charged;
808 $balance -= $_->amount foreach ( $self->cust_bill_pay );
809 $balance -= $_->amount foreach ( $self->cust_credited );
810 $balance = sprintf( "%.2f", $balance);
811 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
816 my( $self, $pkgnum ) = @_;
818 #my $balance = $self->charged;
820 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
822 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
823 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
825 $balance = sprintf( "%.2f", $balance);
826 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
832 Returns true if this invoice should be hidden. See the
833 selfservice-hide_invoices-taxclass configuraiton setting.
839 my $conf = $self->conf;
840 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
842 my @cust_bill_pkg = $self->cust_bill_pkg;
843 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
844 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
847 =item apply_payments_and_credits [ OPTION => VALUE ... ]
849 Applies unapplied payments and credits to this invoice.
851 A hash of optional arguments may be passed. Currently "manual" is supported.
852 If true, a payment receipt is sent instead of a statement when
853 'payment_receipt_email' configuration option is set.
855 If there is an error, returns the error, otherwise returns false.
859 sub apply_payments_and_credits {
860 my( $self, %options ) = @_;
861 my $conf = $self->conf;
863 local $SIG{HUP} = 'IGNORE';
864 local $SIG{INT} = 'IGNORE';
865 local $SIG{QUIT} = 'IGNORE';
866 local $SIG{TERM} = 'IGNORE';
867 local $SIG{TSTP} = 'IGNORE';
868 local $SIG{PIPE} = 'IGNORE';
870 my $oldAutoCommit = $FS::UID::AutoCommit;
871 local $FS::UID::AutoCommit = 0;
874 $self->select_for_update; #mutex
876 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
877 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
879 if ( $conf->exists('pkg-balances') ) {
880 # limit @payments & @credits to those w/ a pkgnum grepped from $self
881 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
882 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
883 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
886 while ( $self->owed > 0 and ( @payments || @credits ) ) {
889 if ( @payments && @credits ) {
891 #decide which goes first by weight of top (unapplied) line item
893 my @open_lineitems = $self->open_cust_bill_pkg;
896 max( map { $_->part_pkg->pay_weight || 0 }
901 my $max_credit_weight =
902 max( map { $_->part_pkg->credit_weight || 0 }
908 #if both are the same... payments first? it has to be something
909 if ( $max_pay_weight >= $max_credit_weight ) {
915 } elsif ( @payments ) {
917 } elsif ( @credits ) {
920 die "guru meditation #12 and 35";
924 if ( $app eq 'pay' ) {
926 my $payment = shift @payments;
927 $unapp_amount = $payment->unapplied;
928 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
929 $app->pkgnum( $payment->pkgnum )
930 if $conf->exists('pkg-balances') && $payment->pkgnum;
932 } elsif ( $app eq 'credit' ) {
934 my $credit = shift @credits;
935 $unapp_amount = $credit->credited;
936 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
937 $app->pkgnum( $credit->pkgnum )
938 if $conf->exists('pkg-balances') && $credit->pkgnum;
941 die "guru meditation #12 and 35";
945 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
946 warn "owed_pkgnum ". $app->pkgnum;
947 $owed = $self->owed_pkgnum($app->pkgnum);
951 next unless $owed > 0;
953 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
954 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
956 $app->invnum( $self->invnum );
958 my $error = $app->insert(%options);
960 $dbh->rollback if $oldAutoCommit;
961 return "Error inserting ". $app->table. " record: $error";
963 die $error if $error;
967 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
972 =item generate_email OPTION => VALUE ...
980 sender address, required
984 alternate template name, optional
988 text attachment arrayref, optional
992 email subject, optional
996 notice name instead of "Invoice", optional
1000 Returns an argument list to be passed to L<FS::Misc::send_email>.
1006 sub generate_email {
1010 my $conf = $self->conf;
1012 my $me = '[FS::cust_bill::generate_email]';
1015 'from' => $args{'from'},
1016 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1020 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1021 'template' => $args{'template'},
1022 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1023 'no_coupon' => $args{'no_coupon'},
1026 my $cust_main = $self->cust_main;
1028 if (ref($args{'to'}) eq 'ARRAY') {
1029 $return{'to'} = $args{'to'};
1031 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1032 $cust_main->invoicing_list
1036 if ( $conf->exists('invoice_html') ) {
1038 warn "$me creating HTML/text multipart message"
1041 $return{'nobody'} = 1;
1043 my $alternative = build MIME::Entity
1044 'Type' => 'multipart/alternative',
1045 #'Encoding' => '7bit',
1046 'Disposition' => 'inline'
1050 if ( $conf->exists('invoice_email_pdf')
1051 and scalar($conf->config('invoice_email_pdf_note')) ) {
1053 warn "$me using 'invoice_email_pdf_note' in multipart message"
1055 $data = [ map { $_ . "\n" }
1056 $conf->config('invoice_email_pdf_note')
1061 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1063 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1064 $data = $args{'print_text'};
1066 $data = [ $self->print_text(\%opt) ];
1071 $alternative->attach(
1072 'Type' => 'text/plain',
1073 'Encoding' => 'quoted-printable',
1074 #'Encoding' => '7bit',
1076 'Disposition' => 'inline',
1083 if ( $conf->exists('invoice_email_pdf')
1084 and scalar($conf->config('invoice_email_pdf_note')) ) {
1086 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1090 $args{'from'} =~ /\@([\w\.\-]+)/;
1091 my $from = $1 || 'example.com';
1092 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1095 my $agentnum = $cust_main->agentnum;
1096 if ( defined($args{'template'}) && length($args{'template'})
1097 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1100 $logo = 'logo_'. $args{'template'}. '.png';
1104 my $image_data = $conf->config_binary( $logo, $agentnum);
1106 $image = build MIME::Entity
1107 'Type' => 'image/png',
1108 'Encoding' => 'base64',
1109 'Data' => $image_data,
1110 'Filename' => 'logo.png',
1111 'Content-ID' => "<$content_id>",
1114 if ($conf->exists('invoice-barcode')) {
1115 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1116 $barcode = build MIME::Entity
1117 'Type' => 'image/png',
1118 'Encoding' => 'base64',
1119 'Data' => $self->invoice_barcode(0),
1120 'Filename' => 'barcode.png',
1121 'Content-ID' => "<$barcode_content_id>",
1123 $opt{'barcode_cid'} = $barcode_content_id;
1126 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1129 $alternative->attach(
1130 'Type' => 'text/html',
1131 'Encoding' => 'quoted-printable',
1132 'Data' => [ '<html>',
1135 ' '. encode_entities($return{'subject'}),
1138 ' <body bgcolor="#e8e8e8">',
1143 'Disposition' => 'inline',
1144 #'Filename' => 'invoice.pdf',
1148 my @otherparts = ();
1149 if ( $cust_main->email_csv_cdr ) {
1151 push @otherparts, build MIME::Entity
1152 'Type' => 'text/csv',
1153 'Encoding' => '7bit',
1154 'Data' => [ map { "$_\n" }
1155 $self->call_details('prepend_billed_number' => 1)
1157 'Disposition' => 'attachment',
1158 'Filename' => 'usage-'. $self->invnum. '.csv',
1163 if ( $conf->exists('invoice_email_pdf') ) {
1168 # multipart/alternative
1174 my $related = build MIME::Entity 'Type' => 'multipart/related',
1175 'Encoding' => '7bit';
1177 #false laziness w/Misc::send_email
1178 $related->head->replace('Content-type',
1179 $related->mime_type.
1180 '; boundary="'. $related->head->multipart_boundary. '"'.
1181 '; type=multipart/alternative'
1184 $related->add_part($alternative);
1186 $related->add_part($image) if $image;
1188 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1190 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1194 #no other attachment:
1196 # multipart/alternative
1201 $return{'content-type'} = 'multipart/related';
1202 if ($conf->exists('invoice-barcode') && $barcode) {
1203 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1205 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1207 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1208 #$return{'disposition'} = 'inline';
1214 if ( $conf->exists('invoice_email_pdf') ) {
1215 warn "$me creating PDF attachment"
1218 #mime parts arguments a la MIME::Entity->build().
1219 $return{'mimeparts'} = [
1220 { $self->mimebuild_pdf(\%opt) }
1224 if ( $conf->exists('invoice_email_pdf')
1225 and scalar($conf->config('invoice_email_pdf_note')) ) {
1227 warn "$me using 'invoice_email_pdf_note'"
1229 $return{'body'} = [ map { $_ . "\n" }
1230 $conf->config('invoice_email_pdf_note')
1235 warn "$me not using 'invoice_email_pdf_note'"
1237 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1238 $return{'body'} = $args{'print_text'};
1240 $return{'body'} = [ $self->print_text(\%opt) ];
1253 Returns a list suitable for passing to MIME::Entity->build(), representing
1254 this invoice as PDF attachment.
1261 'Type' => 'application/pdf',
1262 'Encoding' => 'base64',
1263 'Data' => [ $self->print_pdf(@_) ],
1264 'Disposition' => 'attachment',
1265 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1269 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1271 Sends this invoice to the destinations configured for this customer: sends
1272 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1274 Options can be passed as a hashref (recommended) or as a list of up to
1275 four values for templatename, agentnum, invoice_from and amount.
1277 I<template>, if specified, is the name of a suffix for alternate invoices.
1279 I<agentnum>, if specified, means that this invoice will only be sent for customers
1280 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1281 single agent) or an arrayref of agentnums.
1283 I<invoice_from>, if specified, overrides the default email invoice From: address.
1285 I<amount>, if specified, only sends the invoice if the total amount owed on this
1286 invoice and all older invoices is greater than the specified amount.
1288 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1292 sub queueable_send {
1295 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1296 or die "invalid invoice number: " . $opt{invnum};
1298 my @args = ( $opt{template}, $opt{agentnum} );
1299 push @args, $opt{invoice_from}
1300 if exists($opt{invoice_from}) && $opt{invoice_from};
1302 my $error = $self->send( @args );
1303 die $error if $error;
1309 my $conf = $self->conf;
1311 my( $template, $invoice_from, $notice_name );
1313 my $balance_over = 0;
1317 $template = $opt->{'template'} || '';
1318 if ( $agentnums = $opt->{'agentnum'} ) {
1319 $agentnums = [ $agentnums ] unless ref($agentnums);
1321 $invoice_from = $opt->{'invoice_from'};
1322 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1323 $notice_name = $opt->{'notice_name'};
1325 $template = scalar(@_) ? shift : '';
1326 if ( scalar(@_) && $_[0] ) {
1327 $agentnums = ref($_[0]) ? shift : [ shift ];
1329 $invoice_from = shift if scalar(@_);
1330 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1333 my $cust_main = $self->cust_main;
1335 return 'N/A' unless ! $agentnums
1336 or grep { $_ == $cust_main->agentnum } @$agentnums;
1339 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1341 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1342 $conf->config('invoice_from', $cust_main->agentnum );
1345 'template' => $template,
1346 'invoice_from' => $invoice_from,
1347 'notice_name' => ( $notice_name || 'Invoice' ),
1350 my @invoicing_list = $cust_main->invoicing_list;
1352 #$self->email_invoice(\%opt)
1354 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1355 && ! $self->invoice_noemail;
1357 #$self->print_invoice(\%opt)
1359 if grep { $_ eq 'POST' } @invoicing_list; #postal
1361 $self->fax_invoice(\%opt)
1362 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1368 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1370 Emails this invoice.
1372 Options can be passed as a hashref (recommended) or as a list of up to
1373 two values for templatename and invoice_from.
1375 I<template>, if specified, is the name of a suffix for alternate invoices.
1377 I<invoice_from>, if specified, overrides the default email invoice From: address.
1379 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1383 sub queueable_email {
1386 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1387 or die "invalid invoice number: " . $opt{invnum};
1389 my %args = ( 'template' => $opt{template} );
1390 $args{$_} = $opt{$_}
1391 foreach grep { exists($opt{$_}) && $opt{$_} }
1392 qw( invoice_from notice_name no_coupon );
1394 my $error = $self->email( \%args );
1395 die $error if $error;
1399 #sub email_invoice {
1402 return if $self->hide;
1403 my $conf = $self->conf;
1405 my( $template, $invoice_from, $notice_name, $no_coupon );
1408 $template = $opt->{'template'} || '';
1409 $invoice_from = $opt->{'invoice_from'};
1410 $notice_name = $opt->{'notice_name'} || 'Invoice';
1411 $no_coupon = $opt->{'no_coupon'} || 0;
1413 $template = scalar(@_) ? shift : '';
1414 $invoice_from = shift if scalar(@_);
1415 $notice_name = 'Invoice';
1419 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1420 $conf->config('invoice_from', $self->cust_main->agentnum );
1422 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1423 $self->cust_main->invoicing_list;
1425 if ( ! @invoicing_list ) { #no recipients
1426 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1427 die 'No recipients for customer #'. $self->custnum;
1429 #default: better to notify this person than silence
1430 @invoicing_list = ($invoice_from);
1434 my $subject = $self->email_subject($template);
1436 my $error = send_email(
1437 $self->generate_email(
1438 'from' => $invoice_from,
1439 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1440 'subject' => $subject,
1441 'template' => $template,
1442 'notice_name' => $notice_name,
1443 'no_coupon' => $no_coupon,
1446 die "can't email invoice: $error\n" if $error;
1447 #die "$error\n" if $error;
1453 my $conf = $self->conf;
1455 #my $template = scalar(@_) ? shift : '';
1458 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1461 my $cust_main = $self->cust_main;
1462 my $name = $cust_main->name;
1463 my $name_short = $cust_main->name_short;
1464 my $invoice_number = $self->invnum;
1465 my $invoice_date = $self->_date_pretty;
1467 eval qq("$subject");
1470 =item lpr_data HASHREF | [ TEMPLATE ]
1472 Returns the postscript or plaintext for this invoice as an arrayref.
1474 Options can be passed as a hashref (recommended) or as a single optional value
1477 I<template>, if specified, is the name of a suffix for alternate invoices.
1479 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1485 my $conf = $self->conf;
1486 my( $template, $notice_name );
1489 $template = $opt->{'template'} || '';
1490 $notice_name = $opt->{'notice_name'} || 'Invoice';
1492 $template = scalar(@_) ? shift : '';
1493 $notice_name = 'Invoice';
1497 'template' => $template,
1498 'notice_name' => $notice_name,
1501 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1502 [ $self->$method( \%opt ) ];
1505 =item print HASHREF | [ TEMPLATE ]
1507 Prints this invoice.
1509 Options can be passed as a hashref (recommended) or as a single optional
1512 I<template>, if specified, is the name of a suffix for alternate invoices.
1514 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1518 #sub print_invoice {
1521 return if $self->hide;
1522 my $conf = $self->conf;
1524 my( $template, $notice_name );
1527 $template = $opt->{'template'} || '';
1528 $notice_name = $opt->{'notice_name'} || 'Invoice';
1530 $template = scalar(@_) ? shift : '';
1531 $notice_name = 'Invoice';
1535 'template' => $template,
1536 'notice_name' => $notice_name,
1539 if($conf->exists('invoice_print_pdf')) {
1540 # Add the invoice to the current batch.
1541 $self->batch_invoice(\%opt);
1544 do_print $self->lpr_data(\%opt);
1548 =item fax_invoice HASHREF | [ TEMPLATE ]
1552 Options can be passed as a hashref (recommended) or as a single optional
1555 I<template>, if specified, is the name of a suffix for alternate invoices.
1557 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1563 return if $self->hide;
1564 my $conf = $self->conf;
1566 my( $template, $notice_name );
1569 $template = $opt->{'template'} || '';
1570 $notice_name = $opt->{'notice_name'} || 'Invoice';
1572 $template = scalar(@_) ? shift : '';
1573 $notice_name = 'Invoice';
1576 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1577 unless $conf->exists('invoice_latex');
1579 my $dialstring = $self->cust_main->getfield('fax');
1583 'template' => $template,
1584 'notice_name' => $notice_name,
1587 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1588 'dialstring' => $dialstring,
1590 die $error if $error;
1594 =item batch_invoice [ HASHREF ]
1596 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1597 isn't an open batch, one will be created.
1602 my ($self, $opt) = @_;
1603 my $bill_batch = $self->get_open_bill_batch;
1604 my $cust_bill_batch = FS::cust_bill_batch->new({
1605 batchnum => $bill_batch->batchnum,
1606 invnum => $self->invnum,
1608 return $cust_bill_batch->insert($opt);
1611 =item get_open_batch
1613 Returns the currently open batch as an FS::bill_batch object, creating a new
1614 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1619 sub get_open_bill_batch {
1621 my $conf = $self->conf;
1622 my $hashref = { status => 'O' };
1623 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1624 ? $self->cust_main->agentnum
1626 my $batch = qsearchs('bill_batch', $hashref);
1627 return $batch if $batch;
1628 $batch = FS::bill_batch->new($hashref);
1629 my $error = $batch->insert;
1630 die $error if $error;
1634 =item ftp_invoice [ TEMPLATENAME ]
1636 Sends this invoice data via FTP.
1638 TEMPLATENAME is unused?
1644 my $conf = $self->conf;
1645 my $template = scalar(@_) ? shift : '';
1648 'protocol' => 'ftp',
1649 'server' => $conf->config('cust_bill-ftpserver'),
1650 'username' => $conf->config('cust_bill-ftpusername'),
1651 'password' => $conf->config('cust_bill-ftppassword'),
1652 'dir' => $conf->config('cust_bill-ftpdir'),
1653 'format' => $conf->config('cust_bill-ftpformat'),
1657 =item spool_invoice [ TEMPLATENAME ]
1659 Spools this invoice data (see L<FS::spool_csv>)
1661 TEMPLATENAME is unused?
1667 my $conf = $self->conf;
1668 my $template = scalar(@_) ? shift : '';
1671 'format' => $conf->config('cust_bill-spoolformat'),
1672 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1676 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1678 Like B<send>, but only sends the invoice if it is the newest open invoice for
1683 sub send_if_newest {
1688 grep { $_->owed > 0 }
1689 qsearch('cust_bill', {
1690 'custnum' => $self->custnum,
1691 #'_date' => { op=>'>', value=>$self->_date },
1692 'invnum' => { op=>'>', value=>$self->invnum },
1699 =item send_csv OPTION => VALUE, ...
1701 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1705 protocol - currently only "ftp"
1711 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1712 and YYMMDDHHMMSS is a timestamp.
1714 See L</print_csv> for a description of the output format.
1719 my($self, %opt) = @_;
1723 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1724 mkdir $spooldir, 0700 unless -d $spooldir;
1726 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1727 my $file = "$spooldir/$tracctnum.csv";
1729 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1731 open(CSV, ">$file") or die "can't open $file: $!";
1739 if ( $opt{protocol} eq 'ftp' ) {
1740 eval "use Net::FTP;";
1742 $net = Net::FTP->new($opt{server}) or die @$;
1744 die "unknown protocol: $opt{protocol}";
1747 $net->login( $opt{username}, $opt{password} )
1748 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1750 $net->binary or die "can't set binary mode";
1752 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1754 $net->put($file) or die "can't put $file: $!";
1764 Spools CSV invoice data.
1770 =item format - 'default' or 'billco'
1772 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1774 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1776 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1783 my($self, %opt) = @_;
1785 my $cust_main = $self->cust_main;
1787 if ( $opt{'dest'} ) {
1788 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1789 $cust_main->invoicing_list;
1790 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1791 || ! keys %invoicing_list;
1794 if ( $opt{'balanceover'} ) {
1796 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1799 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1800 mkdir $spooldir, 0700 unless -d $spooldir;
1802 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1806 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1807 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1810 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1812 open(CSV, ">>$file") or die "can't open $file: $!";
1813 flock(CSV, LOCK_EX);
1818 if ( lc($opt{'format'}) eq 'billco' ) {
1820 flock(CSV, LOCK_UN);
1825 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1828 open(CSV,">>$file") or die "can't open $file: $!";
1829 flock(CSV, LOCK_EX);
1835 flock(CSV, LOCK_UN);
1842 =item print_csv OPTION => VALUE, ...
1844 Returns CSV data for this invoice.
1848 format - 'default' or 'billco'
1850 Returns a list consisting of two scalars. The first is a single line of CSV
1851 header information for this invoice. The second is one or more lines of CSV
1852 detail information for this invoice.
1854 If I<format> is not specified or "default", the fields of the CSV file are as
1857 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1861 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1863 B<record_type> is C<cust_bill> for the initial header line only. The
1864 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1865 fields are filled in.
1867 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1868 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1871 =item invnum - invoice number
1873 =item custnum - customer number
1875 =item _date - invoice date
1877 =item charged - total invoice amount
1879 =item first - customer first name
1881 =item last - customer first name
1883 =item company - company name
1885 =item address1 - address line 1
1887 =item address2 - address line 1
1897 =item pkg - line item description
1899 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1901 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1903 =item sdate - start date for recurring fee
1905 =item edate - end date for recurring fee
1909 If I<format> is "billco", the fields of the header CSV file are as follows:
1911 +-------------------------------------------------------------------+
1912 | FORMAT HEADER FILE |
1913 |-------------------------------------------------------------------|
1914 | Field | Description | Name | Type | Width |
1915 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1916 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1917 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1918 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1919 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1920 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1921 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1922 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1923 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1924 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1925 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1926 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1927 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1928 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1929 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1930 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1931 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1932 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1933 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1934 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1935 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1936 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1937 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1938 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1939 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1940 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1941 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1942 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1943 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1944 +-------+-------------------------------+------------+------+-------+
1946 If I<format> is "billco", the fields of the detail CSV file are as follows:
1948 FORMAT FOR DETAIL FILE
1950 Field | Description | Name | Type | Width
1951 1 | N/A-Leave Empty | RC | CHAR | 2
1952 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1953 3 | Account Number | TRACCTNUM | CHAR | 15
1954 4 | Invoice Number | TRINVOICE | CHAR | 15
1955 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1956 6 | Transaction Detail | DETAILS | CHAR | 100
1957 7 | Amount | AMT | NUM* | 9
1958 8 | Line Format Control** | LNCTRL | CHAR | 2
1959 9 | Grouping Code | GROUP | CHAR | 2
1960 10 | User Defined | ACCT CODE | CHAR | 15
1965 my($self, %opt) = @_;
1967 eval "use Text::CSV_XS";
1970 my $cust_main = $self->cust_main;
1972 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1974 if ( lc($opt{'format'}) eq 'billco' ) {
1977 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1979 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1981 my( $previous_balance, @unused ) = $self->previous; #previous balance
1983 my $pmt_cr_applied = 0;
1984 $pmt_cr_applied += $_->{'amount'}
1985 foreach ( $self->_items_payments, $self->_items_credits ) ;
1987 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1990 '', # 1 | N/A-Leave Empty CHAR 2
1991 '', # 2 | N/A-Leave Empty CHAR 15
1992 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1993 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1994 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1995 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1996 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1997 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1998 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1999 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2000 '', # 10 | Ancillary Billing Information CHAR 30
2001 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2002 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2005 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2008 $duedate, # 14 | Bill Due Date CHAR 10
2010 $previous_balance, # 15 | Previous Balance NUM* 9
2011 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2012 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2013 $totaldue, # 18 | Total Amt Due NUM* 9
2014 $totaldue, # 19 | Total Amt Due NUM* 9
2015 '', # 20 | 30 Day Aging NUM* 9
2016 '', # 21 | 60 Day Aging NUM* 9
2017 '', # 22 | 90 Day Aging NUM* 9
2018 'N', # 23 | Y/N CHAR 1
2019 '', # 24 | Remittance automation CHAR 100
2020 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2021 $self->custnum, # 26 | Customer Reference Number CHAR 15
2022 '0', # 27 | Federal Tax*** NUM* 9
2023 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2024 '0', # 29 | Other Taxes & Fees*** NUM* 9
2027 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2029 my ($previous_balance) = $self->previous;
2030 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2032 ($_->{pkgnum} || ''),
2035 } $self->_items_pkg;
2038 $cust_main->agentnum,
2039 $cust_main->agent->agent,
2043 $cust_main->address1,
2044 $cust_main->address2,
2050 time2str("%x", $self->_date),
2064 time2str("%x", $self->_date),
2065 sprintf("%.2f", $self->charged),
2066 ( map { $cust_main->getfield($_) }
2067 qw( first last company address1 address2 city state zip country ) ),
2069 ) or die "can't create csv";
2072 my $header = $csv->string. "\n";
2075 if ( lc($opt{'format'}) eq 'billco' ) {
2078 foreach my $item ( $self->_items_pkg ) {
2081 '', # 1 | N/A-Leave Empty CHAR 2
2082 '', # 2 | N/A-Leave Empty CHAR 15
2083 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2084 $self->invnum, # 4 | Invoice Number CHAR 15
2085 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2086 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2087 $item->{'amount'}, # 7 | Amount NUM* 9
2088 '', # 8 | Line Format Control** CHAR 2
2089 '', # 9 | Grouping Code CHAR 2
2090 '', # 10 | User Defined CHAR 15
2093 $detail .= $csv->string. "\n";
2097 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2103 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2105 my($pkg, $setup, $recur, $sdate, $edate);
2106 if ( $cust_bill_pkg->pkgnum ) {
2108 ($pkg, $setup, $recur, $sdate, $edate) = (
2109 $cust_bill_pkg->part_pkg->pkg,
2110 ( $cust_bill_pkg->setup != 0
2111 ? sprintf("%.2f", $cust_bill_pkg->setup )
2113 ( $cust_bill_pkg->recur != 0
2114 ? sprintf("%.2f", $cust_bill_pkg->recur )
2116 ( $cust_bill_pkg->sdate
2117 ? time2str("%x", $cust_bill_pkg->sdate)
2119 ($cust_bill_pkg->edate
2120 ?time2str("%x", $cust_bill_pkg->edate)
2124 } else { #pkgnum tax
2125 next unless $cust_bill_pkg->setup != 0;
2126 $pkg = $cust_bill_pkg->desc;
2127 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2128 ( $sdate, $edate ) = ( '', '' );
2134 ( map { '' } (1..11) ),
2135 ($pkg, $setup, $recur, $sdate, $edate)
2136 ) or die "can't create csv";
2138 $detail .= $csv->string. "\n";
2144 ( $header, $detail );
2150 Pays this invoice with a compliemntary payment. If there is an error,
2151 returns the error, otherwise returns false.
2157 my $cust_pay = new FS::cust_pay ( {
2158 'invnum' => $self->invnum,
2159 'paid' => $self->owed,
2162 'payinfo' => $self->cust_main->payinfo,
2170 Attempts to pay this invoice with a credit card payment via a
2171 Business::OnlinePayment realtime gateway. See
2172 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2173 for supported processors.
2179 $self->realtime_bop( 'CC', @_ );
2184 Attempts to pay this invoice with an electronic check (ACH) payment via a
2185 Business::OnlinePayment realtime gateway. See
2186 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2187 for supported processors.
2193 $self->realtime_bop( 'ECHECK', @_ );
2198 Attempts to pay this invoice with phone bill (LEC) payment via a
2199 Business::OnlinePayment realtime gateway. See
2200 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2201 for supported processors.
2207 $self->realtime_bop( 'LEC', @_ );
2211 my( $self, $method ) = (shift,shift);
2212 my $conf = $self->conf;
2215 my $cust_main = $self->cust_main;
2216 my $balance = $cust_main->balance;
2217 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2218 $amount = sprintf("%.2f", $amount);
2219 return "not run (balance $balance)" unless $amount > 0;
2221 my $description = 'Internet Services';
2222 if ( $conf->exists('business-onlinepayment-description') ) {
2223 my $dtempl = $conf->config('business-onlinepayment-description');
2225 my $agent_obj = $cust_main->agent
2226 or die "can't retreive agent for $cust_main (agentnum ".
2227 $cust_main->agentnum. ")";
2228 my $agent = $agent_obj->agent;
2229 my $pkgs = join(', ',
2230 map { $_->part_pkg->pkg }
2231 grep { $_->pkgnum } $self->cust_bill_pkg
2233 $description = eval qq("$dtempl");
2236 $cust_main->realtime_bop($method, $amount,
2237 'description' => $description,
2238 'invnum' => $self->invnum,
2239 #this didn't do what we want, it just calls apply_payments_and_credits
2241 'apply_to_invoice' => 1,
2244 #this changes application behavior: auto payments
2245 #triggered against a specific invoice are now applied
2246 #to that invoice instead of oldest open.
2252 =item batch_card OPTION => VALUE...
2254 Adds a payment for this invoice to the pending credit card batch (see
2255 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2256 runs the payment using a realtime gateway.
2261 my ($self, %options) = @_;
2262 my $cust_main = $self->cust_main;
2264 $options{invnum} = $self->invnum;
2266 $cust_main->batch_card(%options);
2269 sub _agent_template {
2271 $self->cust_main->agent_template;
2274 sub _agent_invoice_from {
2276 $self->cust_main->agent_invoice_from;
2279 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2281 Returns an text invoice, as a list of lines.
2283 Options can be passed as a hashref (recommended) or as a list of time, template
2284 and then any key/value pairs for any other options.
2286 I<time>, if specified, is used to control the printing of overdue messages. The
2287 default is now. It isn't the date of the invoice; that's the `_date' field.
2288 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2289 L<Time::Local> and L<Date::Parse> for conversion functions.
2291 I<template>, if specified, is the name of a suffix for alternate invoices.
2293 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2299 my( $today, $template, %opt );
2301 %opt = %{ shift() };
2302 $today = delete($opt{'time'}) || '';
2303 $template = delete($opt{template}) || '';
2305 ( $today, $template, %opt ) = @_;
2308 my %params = ( 'format' => 'template' );
2309 $params{'time'} = $today if $today;
2310 $params{'template'} = $template if $template;
2311 $params{$_} = $opt{$_}
2312 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2314 $self->print_generic( %params );
2317 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2319 Internal method - returns a filename of a filled-in LaTeX template for this
2320 invoice (Note: add ".tex" to get the actual filename), and a filename of
2321 an associated logo (with the .eps extension included).
2323 See print_ps and print_pdf for methods that return PostScript and PDF output.
2325 Options can be passed as a hashref (recommended) or as a list of time, template
2326 and then any key/value pairs for any other options.
2328 I<time>, if specified, is used to control the printing of overdue messages. The
2329 default is now. It isn't the date of the invoice; that's the `_date' field.
2330 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2331 L<Time::Local> and L<Date::Parse> for conversion functions.
2333 I<template>, if specified, is the name of a suffix for alternate invoices.
2335 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2341 my $conf = $self->conf;
2342 my( $today, $template, %opt );
2344 %opt = %{ shift() };
2345 $today = delete($opt{'time'}) || '';
2346 $template = delete($opt{template}) || '';
2348 ( $today, $template, %opt ) = @_;
2351 my %params = ( 'format' => 'latex' );
2352 $params{'time'} = $today if $today;
2353 $params{'template'} = $template if $template;
2354 $params{$_} = $opt{$_}
2355 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2357 $template ||= $self->_agent_template;
2359 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2360 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2364 ) or die "can't open temp file: $!\n";
2366 my $agentnum = $self->cust_main->agentnum;
2368 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2369 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2370 or die "can't write temp file: $!\n";
2372 print $lh $conf->config_binary('logo.eps', $agentnum)
2373 or die "can't write temp file: $!\n";
2376 $params{'logo_file'} = $lh->filename;
2378 if($conf->exists('invoice-barcode')){
2379 my $png_file = $self->invoice_barcode($dir);
2380 my $eps_file = $png_file;
2381 $eps_file =~ s/\.png$/.eps/g;
2382 $png_file =~ /(barcode.*png)/;
2384 $eps_file =~ /(barcode.*eps)/;
2387 my $curr_dir = cwd();
2389 # after painfuly long experimentation, it was determined that sam2p won't
2390 # accept : and other chars in the path, no matter how hard I tried to
2391 # escape them, hence the chdir (and chdir back, just to be safe)
2392 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2393 or die "sam2p failed: $!\n";
2397 $params{'barcode_file'} = $eps_file;
2400 my @filled_in = $self->print_generic( %params );
2402 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2406 ) or die "can't open temp file: $!\n";
2407 binmode($fh, ':utf8'); # language support
2408 print $fh join('', @filled_in );
2411 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2412 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2416 =item invoice_barcode DIR_OR_FALSE
2418 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2419 it is taken as the temp directory where the PNG file will be generated and the
2420 PNG file name is returned. Otherwise, the PNG image itself is returned.
2424 sub invoice_barcode {
2425 my ($self, $dir) = (shift,shift);
2427 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2428 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2429 my $gd = $gdbar->plot(Height => 30);
2432 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2436 ) or die "can't open temp file: $!\n";
2437 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2438 my $png_file = $bh->filename;
2445 =item print_generic OPTION => VALUE ...
2447 Internal method - returns a filled-in template for this invoice as a scalar.
2449 See print_ps and print_pdf for methods that return PostScript and PDF output.
2451 Non optional options include
2452 format - latex, html, template
2454 Optional options include
2456 template - a value used as a suffix for a configuration template
2458 time - a value used to control the printing of overdue messages. The
2459 default is now. It isn't the date of the invoice; that's the `_date' field.
2460 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2461 L<Time::Local> and L<Date::Parse> for conversion functions.
2465 unsquelch_cdr - overrides any per customer cdr squelching when true
2467 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2469 locale - override customer's locale
2473 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2474 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2475 # yes: fixed width/plain text printing will be borked
2477 my( $self, %params ) = @_;
2478 my $conf = $self->conf;
2479 my $today = $params{today} ? $params{today} : time;
2480 warn "$me print_generic called on $self with suffix $params{template}\n"
2483 my $format = $params{format};
2484 die "Unknown format: $format"
2485 unless $format =~ /^(latex|html|template)$/;
2487 my $cust_main = $self->cust_main;
2488 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2489 unless $cust_main->payname
2490 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2492 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2493 'html' => [ '<%=', '%>' ],
2494 'template' => [ '{', '}' ],
2497 warn "$me print_generic creating template\n"
2500 #create the template
2501 my $template = $params{template} ? $params{template} : $self->_agent_template;
2502 my $templatefile = "invoice_$format";
2503 $templatefile .= "_$template"
2504 if length($template) && $conf->exists($templatefile."_$template");
2505 my @invoice_template = map "$_\n", $conf->config($templatefile)
2506 or die "cannot load config data $templatefile";
2509 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2510 #change this to a die when the old code is removed
2511 warn "old-style invoice template $templatefile; ".
2512 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2513 $old_latex = 'true';
2514 @invoice_template = _translate_old_latex_format(@invoice_template);
2517 warn "$me print_generic creating T:T object\n"
2520 my $text_template = new Text::Template(
2522 SOURCE => \@invoice_template,
2523 DELIMITERS => $delimiters{$format},
2526 warn "$me print_generic compiling T:T object\n"
2529 $text_template->compile()
2530 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2533 # additional substitution could possibly cause breakage in existing templates
2534 my %convert_maps = (
2536 'notes' => sub { map "$_", @_ },
2537 'footer' => sub { map "$_", @_ },
2538 'smallfooter' => sub { map "$_", @_ },
2539 'returnaddress' => sub { map "$_", @_ },
2540 'coupon' => sub { map "$_", @_ },
2541 'summary' => sub { map "$_", @_ },
2547 s/%%(.*)$/<!-- $1 -->/g;
2548 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2549 s/\\begin\{enumerate\}/<ol>/g;
2551 s/\\end\{enumerate\}/<\/ol>/g;
2552 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2561 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2563 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2568 s/\\\\\*?\s*$/<BR>/;
2569 s/\\hyphenation\{[\w\s\-]+}//;
2574 'coupon' => sub { "" },
2575 'summary' => sub { "" },
2582 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2583 s/\\begin\{enumerate\}//g;
2585 s/\\end\{enumerate\}//g;
2586 s/\\textbf\{(.*)\}/$1/g;
2593 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2595 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2600 s/\\\\\*?\s*$/\n/; # dubious
2601 s/\\hyphenation\{[\w\s\-]+}//;
2605 'coupon' => sub { "" },
2606 'summary' => sub { "" },
2611 # hashes for differing output formats
2612 my %nbsps = ( 'latex' => '~',
2613 'html' => '', # '&nbps;' would be nice
2614 'template' => '', # not used
2616 my $nbsp = $nbsps{$format};
2618 my %escape_functions = ( 'latex' => \&_latex_escape,
2619 'html' => \&_html_escape_nbsp,#\&encode_entities,
2620 'template' => sub { shift },
2622 my $escape_function = $escape_functions{$format};
2623 my $escape_function_nonbsp = ($format eq 'html')
2624 ? \&_html_escape : $escape_function;
2626 my %date_formats = ( 'latex' => $date_format_long,
2627 'html' => $date_format_long,
2630 $date_formats{'html'} =~ s/ / /g;
2632 my $date_format = $date_formats{$format};
2634 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2636 'html' => sub { return '<b>'. shift(). '</b>'
2638 'template' => sub { shift },
2640 my $embolden_function = $embolden_functions{$format};
2642 my %newline_tokens = ( 'latex' => '\\\\',
2646 my $newline_token = $newline_tokens{$format};
2648 warn "$me generating template variables\n"
2651 # generate template variables
2654 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2658 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2664 $returnaddress = join("\n",
2665 $conf->config_orbase("invoice_${format}returnaddress", $template)
2668 } elsif ( grep /\S/,
2669 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2671 my $convert_map = $convert_maps{$format}{'returnaddress'};
2674 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2679 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2681 my $convert_map = $convert_maps{$format}{'returnaddress'};
2682 $returnaddress = join( "\n", &$convert_map(
2683 map { s/( {2,})/'~' x length($1)/eg;
2687 ( $conf->config('company_name', $self->cust_main->agentnum),
2688 $conf->config('company_address', $self->cust_main->agentnum),
2695 my $warning = "Couldn't find a return address; ".
2696 "do you need to set the company_address configuration value?";
2698 $returnaddress = $nbsp;
2699 #$returnaddress = $warning;
2703 warn "$me generating invoice data\n"
2706 my $agentnum = $self->cust_main->agentnum;
2708 my %invoice_data = (
2711 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2712 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2713 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2714 'returnaddress' => $returnaddress,
2715 'agent' => &$escape_function($cust_main->agent->agent),
2718 'invnum' => $self->invnum,
2719 'date' => time2str($date_format, $self->_date),
2720 'today' => time2str($date_format_long, $today),
2721 'terms' => $self->terms,
2722 'template' => $template, #params{'template'},
2723 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2724 'current_charges' => sprintf("%.2f", $self->charged),
2725 'duedate' => $self->due_date2str($rdate_format), #date_format?
2728 'custnum' => $cust_main->display_custnum,
2729 'agent_custid' => &$escape_function($cust_main->agent_custid),
2730 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2731 payname company address1 address2 city state zip fax
2735 'ship_enable' => $conf->exists('invoice-ship_address'),
2736 'unitprices' => $conf->exists('invoice-unitprice'),
2737 'smallernotes' => $conf->exists('invoice-smallernotes'),
2738 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2739 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2741 #layout info -- would be fancy to calc some of this and bury the template
2743 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2744 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2745 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2746 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2747 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2748 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2749 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2750 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2751 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2752 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2754 # better hang on to conf_dir for a while (for old templates)
2755 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2757 #these are only used when doing paged plaintext
2764 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2765 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2766 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2767 # eval to avoid death for unimplemented languages
2768 my $dh = eval { Date::Language->new($info{'name'}) } ||
2769 Date::Language->new(); # fall back to English
2770 # prototype here to silence warnings
2771 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2772 # eventually use this date handle everywhere in here, too
2774 my $min_sdate = 999999999999;
2776 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2777 next unless $cust_bill_pkg->pkgnum > 0;
2778 $min_sdate = $cust_bill_pkg->sdate
2779 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2780 $max_edate = $cust_bill_pkg->edate
2781 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2784 $invoice_data{'bill_period'} = '';
2785 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2786 . " to " . time2str('%e %h', $max_edate)
2787 if ($max_edate != 0 && $min_sdate != 999999999999);
2789 $invoice_data{finance_section} = '';
2790 if ( $conf->config('finance_pkgclass') ) {
2792 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2793 $invoice_data{finance_section} = $pkg_class->categoryname;
2795 $invoice_data{finance_amount} = '0.00';
2796 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2798 my $countrydefault = $conf->config('countrydefault') || 'US';
2799 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2800 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2801 my $method = $prefix.$_;
2802 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2804 $invoice_data{'ship_country'} = ''
2805 if ( $invoice_data{'ship_country'} eq $countrydefault );
2807 $invoice_data{'cid'} = $params{'cid'}
2810 if ( $cust_main->country eq $countrydefault ) {
2811 $invoice_data{'country'} = '';
2813 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2817 $invoice_data{'address'} = \@address;
2819 $cust_main->payname.
2820 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2821 ? " (P.O. #". $cust_main->payinfo. ")"
2825 push @address, $cust_main->company
2826 if $cust_main->company;
2827 push @address, $cust_main->address1;
2828 push @address, $cust_main->address2
2829 if $cust_main->address2;
2831 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2832 push @address, $invoice_data{'country'}
2833 if $invoice_data{'country'};
2835 while (scalar(@address) < 5);
2837 $invoice_data{'logo_file'} = $params{'logo_file'}
2838 if $params{'logo_file'};
2839 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2840 if $params{'barcode_file'};
2841 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2842 if $params{'barcode_img'};
2843 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2844 if $params{'barcode_cid'};
2846 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2847 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2848 #my $balance_due = $self->owed + $pr_total - $cr_total;
2849 my $balance_due = $self->owed + $pr_total;
2851 # the customer's current balance as shown on the invoice before this one
2852 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2854 # the change in balance from that invoice to this one
2855 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2857 # the sum of amount owed on all previous invoices
2858 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2860 # the sum of amount owed on all invoices
2861 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2863 # info from customer's last invoice before this one, for some
2865 $invoice_data{'last_bill'} = {};
2866 my $last_bill = $pr_cust_bill[-1];
2868 $invoice_data{'last_bill'} = {
2869 '_date' => $last_bill->_date, #unformatted
2870 # all we need for now
2874 my $summarypage = '';
2875 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2878 $invoice_data{'summarypage'} = $summarypage;
2880 warn "$me substituting variables in notes, footer, smallfooter\n"
2883 my @include = (qw( notes footer smallfooter ));
2884 push @include, 'coupon' unless $params{'no_coupon'};
2885 foreach my $include (@include) {
2887 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2890 if ( $conf->exists($inc_file, $agentnum)
2891 && length( $conf->config($inc_file, $agentnum) ) ) {
2893 @inc_src = $conf->config($inc_file, $agentnum);
2897 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2899 my $convert_map = $convert_maps{$format}{$include};
2901 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2902 s/--\@\]/$delimiters{$format}[1]/g;
2905 &$convert_map( $conf->config($inc_file, $agentnum) );
2909 my $inc_tt = new Text::Template (
2911 SOURCE => [ map "$_\n", @inc_src ],
2912 DELIMITERS => $delimiters{$format},
2913 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2915 unless ( $inc_tt->compile() ) {
2916 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2917 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2921 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2923 $invoice_data{$include} =~ s/\n+$//
2924 if ($format eq 'latex');
2927 # let invoices use either of these as needed
2928 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2929 ? $cust_main->payinfo : '';
2930 $invoice_data{'po_line'} =
2931 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2932 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2935 my %money_chars = ( 'latex' => '',
2936 'html' => $conf->config('money_char') || '$',
2939 my $money_char = $money_chars{$format};
2941 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2942 'html' => $conf->config('money_char') || '$',
2945 my $other_money_char = $other_money_chars{$format};
2946 $invoice_data{'dollar'} = $other_money_char;
2948 my @detail_items = ();
2949 my @total_items = ();
2953 $invoice_data{'detail_items'} = \@detail_items;
2954 $invoice_data{'total_items'} = \@total_items;
2955 $invoice_data{'buf'} = \@buf;
2956 $invoice_data{'sections'} = \@sections;
2958 warn "$me generating sections\n"
2961 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2962 'subtotal' => $other_money_char.
2963 sprintf('%.2f', $pr_total),
2964 'summarized' => '', #why? $summarypage ? 'Y' : '',
2966 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2967 join(' / ', map { $cust_main->balance_date_range(@$_) }
2968 $self->_prior_month30s
2970 if $conf->exists('invoice_include_aging');
2973 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2974 'subtotal' => $taxtotal, # adjusted below
2976 my $tax_weight = _pkg_category($tax_section->{description})
2977 ? _pkg_category($tax_section->{description})->weight
2979 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2980 $tax_section->{'sort_weight'} = $tax_weight;
2983 my $adjusttotal = 0;
2984 my $adjust_section = { 'description' =>
2985 $self->mt('Credits, Payments, and Adjustments'),
2986 'subtotal' => 0, # adjusted below
2988 my $adjust_weight = _pkg_category($adjust_section->{description})
2989 ? _pkg_category($adjust_section->{description})->weight
2991 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2992 $adjust_section->{'sort_weight'} = $adjust_weight;
2994 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2995 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2996 $invoice_data{'multisection'} = $multisection;
2997 my $late_sections = [];
2998 my $extra_sections = [];
2999 my $extra_lines = ();
3001 my $default_section = { 'description' => '',
3006 if ( $multisection ) {
3007 ($extra_sections, $extra_lines) =
3008 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3009 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3011 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3013 push @detail_items, @$extra_lines if $extra_lines;
3015 $self->_items_sections( $late_sections, # this could stand a refactor
3017 $escape_function_nonbsp,
3021 if ($conf->exists('svc_phone_sections')) {
3022 my ($phone_sections, $phone_lines) =
3023 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3024 push @{$late_sections}, @$phone_sections;
3025 push @detail_items, @$phone_lines;
3027 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3028 my ($accountcode_section, $accountcode_lines) =
3029 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3030 if ( scalar(@$accountcode_lines) ) {
3031 push @{$late_sections}, $accountcode_section;
3032 push @detail_items, @$accountcode_lines;
3035 } else {# not multisection
3036 # make a default section
3037 push @sections, $default_section;
3038 # and calculate the finance charge total, since it won't get done otherwise.
3039 # XXX possibly other totals?
3040 # XXX possibly finance_pkgclass should not be used in this manner?
3041 if ( $conf->exists('finance_pkgclass') ) {
3042 my @finance_charges;
3043 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3044 if ( grep { $_->section eq $invoice_data{finance_section} }
3045 $cust_bill_pkg->cust_bill_pkg_display ) {
3046 # I think these are always setup fees, but just to be sure...
3047 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3050 $invoice_data{finance_amount} =
3051 sprintf('%.2f', sum( @finance_charges ) || 0);
3055 # previous invoice balances in the Previous Charges section if there
3056 # is one, otherwise in the main detail section
3057 if ( $self->can('_items_previous') &&
3058 $self->enable_previous &&
3059 ! $conf->exists('previous_balance-summary_only') ) {
3061 warn "$me adding previous balances\n"
3064 foreach my $line_item ( $self->_items_previous ) {
3067 ext_description => [],
3069 $detail->{'ref'} = $line_item->{'pkgnum'};
3070 $detail->{'quantity'} = 1;
3071 $detail->{'section'} = $multisection ? $previous_section
3073 $detail->{'description'} = &$escape_function($line_item->{'description'});
3074 if ( exists $line_item->{'ext_description'} ) {
3075 @{$detail->{'ext_description'}} = map {
3076 &$escape_function($_);
3077 } @{$line_item->{'ext_description'}};
3079 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3080 $line_item->{'amount'};
3081 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3083 push @detail_items, $detail;
3084 push @buf, [ $detail->{'description'},
3085 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3091 if ( @pr_cust_bill && $self->enable_previous ) {
3092 push @buf, ['','-----------'];
3093 push @buf, [ $self->mt('Total Previous Balance'),
3094 $money_char. sprintf("%10.2f", $pr_total) ];
3098 if ( $conf->exists('svc_phone-did-summary') ) {
3099 warn "$me adding DID summary\n"
3102 my ($didsummary,$minutes) = $self->_did_summary;
3103 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3105 { 'description' => $didsummary_desc,
3106 'ext_description' => [ $didsummary, $minutes ],
3110 foreach my $section (@sections, @$late_sections) {
3112 warn "$me adding section \n". Dumper($section)
3115 # begin some normalization
3116 $section->{'subtotal'} = $section->{'amount'}
3118 && !exists($section->{subtotal})
3119 && exists($section->{amount});
3121 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3122 if ( $invoice_data{finance_section} &&
3123 $section->{'description'} eq $invoice_data{finance_section} );
3125 $section->{'subtotal'} = $other_money_char.
3126 sprintf('%.2f', $section->{'subtotal'})
3129 # continue some normalization
3130 $section->{'amount'} = $section->{'subtotal'}
3134 if ( $section->{'description'} ) {
3135 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3140 warn "$me setting options\n"
3143 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3145 $options{'section'} = $section if $multisection;
3146 $options{'format'} = $format;
3147 $options{'escape_function'} = $escape_function;
3148 $options{'no_usage'} = 1 unless $unsquelched;
3149 $options{'unsquelched'} = $unsquelched;
3150 $options{'summary_page'} = $summarypage;
3151 $options{'skip_usage'} =
3152 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3153 $options{'multilocation'} = $multilocation;
3154 $options{'multisection'} = $multisection;
3156 warn "$me searching for line items\n"
3159 foreach my $line_item ( $self->_items_pkg(%options) ) {
3161 warn "$me adding line item $line_item\n"
3165 ext_description => [],
3167 $detail->{'ref'} = $line_item->{'pkgnum'};
3168 $detail->{'quantity'} = $line_item->{'quantity'};
3169 $detail->{'section'} = $section;
3170 $detail->{'description'} = &$escape_function($line_item->{'description'});
3171 if ( exists $line_item->{'ext_description'} ) {
3172 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3174 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3175 $line_item->{'amount'};
3176 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3177 $line_item->{'unit_amount'};
3178 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3180 $detail->{'sdate'} = $line_item->{'sdate'};
3181 $detail->{'edate'} = $line_item->{'edate'};
3182 $detail->{'seconds'} = $line_item->{'seconds'};
3184 push @detail_items, $detail;
3185 push @buf, ( [ $detail->{'description'},
3186 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3188 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3192 if ( $section->{'description'} ) {
3193 push @buf, ( ['','-----------'],
3194 [ $section->{'description'}. ' sub-total',
3195 $section->{'subtotal'} # already formatted this
3204 $invoice_data{current_less_finance} =
3205 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3207 # create a major section for previous balance if we have major sections,
3208 # or if previous_section is in summary form
3209 if ( ( $multisection && $self->enable_previous )
3210 || $conf->exists('previous_balance-summary_only') )
3212 unshift @sections, $previous_section if $pr_total;
3215 warn "$me adding taxes\n"
3218 foreach my $tax ( $self->_items_tax ) {
3220 $taxtotal += $tax->{'amount'};
3222 my $description = &$escape_function( $tax->{'description'} );
3223 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3225 if ( $multisection ) {
3227 my $money = $old_latex ? '' : $money_char;
3228 push @detail_items, {
3229 ext_description => [],
3232 description => $description,
3233 amount => $money. $amount,
3235 section => $tax_section,
3240 push @total_items, {
3241 'total_item' => $description,
3242 'total_amount' => $other_money_char. $amount,
3247 push @buf,[ $description,
3248 $money_char. $amount,
3255 $total->{'total_item'} = $self->mt('Sub-total');
3256 $total->{'total_amount'} =
3257 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3259 if ( $multisection ) {
3260 $tax_section->{'subtotal'} = $other_money_char.
3261 sprintf('%.2f', $taxtotal);
3262 $tax_section->{'pretotal'} = 'New charges sub-total '.
3263 $total->{'total_amount'};
3264 push @sections, $tax_section if $taxtotal;
3266 unshift @total_items, $total;
3269 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3271 push @buf,['','-----------'];
3272 push @buf,[$self->mt(
3273 (!$self->enable_previous)
3275 : 'Total New Charges'
3277 $money_char. sprintf("%10.2f",$self->charged) ];
3280 # calculate total, possibly including total owed on previous
3285 $item = $conf->config('previous_balance-exclude_from_total')
3286 || 'Total New Charges'
3287 if $conf->exists('previous_balance-exclude_from_total');
3288 my $amount = $self->charged;
3289 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3290 $amount += $pr_total;
3293 $total->{'total_item'} = &$embolden_function($self->mt($item));
3294 $total->{'total_amount'} =
3295 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3296 if ( $multisection ) {
3297 if ( $adjust_section->{'sort_weight'} ) {
3298 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3299 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3301 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3302 $other_money_char. sprintf('%.2f', $self->charged );
3305 push @total_items, $total;
3307 push @buf,['','-----------'];
3310 sprintf( '%10.2f', $amount )
3315 # if we're showing previous invoices, also show previous
3316 # credits and payments
3317 if ( $self->enable_previous
3318 and $self->can('_items_credits')
3319 and $self->can('_items_payments') )
3321 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3324 my $credittotal = 0;
3325 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3328 $total->{'total_item'} = &$escape_function($credit->{'description'});
3329 $credittotal += $credit->{'amount'};
3330 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3331 $adjusttotal += $credit->{'amount'};
3332 if ( $multisection ) {
3333 my $money = $old_latex ? '' : $money_char;
3334 push @detail_items, {
3335 ext_description => [],
3338 description => &$escape_function($credit->{'description'}),
3339 amount => $money. $credit->{'amount'},
3341 section => $adjust_section,
3344 push @total_items, $total;
3348 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3351 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3352 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3356 my $paymenttotal = 0;
3357 foreach my $payment ( $self->_items_payments ) {
3359 $total->{'total_item'} = &$escape_function($payment->{'description'});
3360 $paymenttotal += $payment->{'amount'};
3361 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3362 $adjusttotal += $payment->{'amount'};
3363 if ( $multisection ) {
3364 my $money = $old_latex ? '' : $money_char;
3365 push @detail_items, {
3366 ext_description => [],
3369 description => &$escape_function($payment->{'description'}),
3370 amount => $money. $payment->{'amount'},
3372 section => $adjust_section,
3375 push @total_items, $total;
3377 push @buf, [ $payment->{'description'},
3378 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3381 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3383 if ( $multisection ) {
3384 $adjust_section->{'subtotal'} = $other_money_char.
3385 sprintf('%.2f', $adjusttotal);
3386 push @sections, $adjust_section
3387 unless $adjust_section->{sort_weight};
3390 # create Balance Due message
3393 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3394 $total->{'total_amount'} =
3395 &$embolden_function(
3396 $other_money_char. sprintf('%.2f', $summarypage
3398 $self->billing_balance
3399 : $self->owed + $pr_total
3402 if ( $multisection && !$adjust_section->{sort_weight} ) {
3403 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3404 $total->{'total_amount'};
3406 push @total_items, $total;
3408 push @buf,['','-----------'];
3409 push @buf,[$self->balance_due_msg, $money_char.
3410 sprintf("%10.2f", $balance_due ) ];
3413 if ( $conf->exists('previous_balance-show_credit')
3414 and $cust_main->balance < 0 ) {
3415 my $credit_total = {
3416 'total_item' => &$embolden_function($self->credit_balance_msg),
3417 'total_amount' => &$embolden_function(
3418 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3421 if ( $multisection ) {
3422 $adjust_section->{'posttotal'} .= $newline_token .
3423 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3426 push @total_items, $credit_total;
3428 push @buf,['','-----------'];
3429 push @buf,[$self->credit_balance_msg, $money_char.
3430 sprintf("%10.2f", -$cust_main->balance ) ];
3434 if ( $multisection ) {
3435 if ($conf->exists('svc_phone_sections')) {
3437 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3438 $total->{'total_amount'} =
3439 &$embolden_function(
3440 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3442 my $last_section = pop @sections;
3443 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3444 $total->{'total_amount'};
3445 push @sections, $last_section;
3447 push @sections, @$late_sections
3451 # make a discounts-available section, even without multisection
3452 if ( $conf->exists('discount-show_available')
3453 and my @discounts_avail = $self->_items_discounts_avail ) {
3454 my $discount_section = {
3455 'description' => $self->mt('Discounts Available'),
3460 push @sections, $discount_section;
3461 push @detail_items, map { +{
3462 'ref' => '', #should this be something else?
3463 'section' => $discount_section,
3464 'description' => &$escape_function( $_->{description} ),
3465 'amount' => $money_char . &$escape_function( $_->{amount} ),
3466 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3467 } } @discounts_avail;
3470 # All sections and items are built; now fill in templates.
3471 my @includelist = ();
3472 push @includelist, 'summary' if $summarypage;
3473 foreach my $include ( @includelist ) {
3475 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3478 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3480 @inc_src = $conf->config($inc_file, $agentnum);
3484 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3486 my $convert_map = $convert_maps{$format}{$include};
3488 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3489 s/--\@\]/$delimiters{$format}[1]/g;
3492 &$convert_map( $conf->config($inc_file, $agentnum) );
3496 my $inc_tt = new Text::Template (
3498 SOURCE => [ map "$_\n", @inc_src ],
3499 DELIMITERS => $delimiters{$format},
3500 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3502 unless ( $inc_tt->compile() ) {
3503 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3504 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3508 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3510 $invoice_data{$include} =~ s/\n+$//
3511 if ($format eq 'latex');
3516 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3517 /invoice_lines\((\d*)\)/;
3518 $invoice_lines += $1 || scalar(@buf);
3521 die "no invoice_lines() functions in template?"
3522 if ( $format eq 'template' && !$wasfunc );
3524 if ($format eq 'template') {
3526 if ( $invoice_lines ) {
3527 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3528 $invoice_data{'total_pages'}++
3529 if scalar(@buf) % $invoice_lines;
3532 #setup subroutine for the template
3533 $invoice_data{invoice_lines} = sub {
3534 my $lines = shift || scalar(@buf);
3546 push @collect, split("\n",
3547 $text_template->fill_in( HASH => \%invoice_data )
3549 $invoice_data{'page'}++;
3551 map "$_\n", @collect;
3553 # this is where we actually create the invoice
3554 warn "filling in template for invoice ". $self->invnum. "\n"
3556 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3559 $text_template->fill_in(HASH => \%invoice_data);
3563 # helper routine for generating date ranges
3564 sub _prior_month30s {
3567 [ 1, 2592000 ], # 0-30 days ago
3568 [ 2592000, 5184000 ], # 30-60 days ago
3569 [ 5184000, 7776000 ], # 60-90 days ago
3570 [ 7776000, 0 ], # 90+ days ago
3573 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3574 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3579 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3581 Returns an postscript invoice, as a scalar.
3583 Options can be passed as a hashref (recommended) or as a list of time, template
3584 and then any key/value pairs for any other options.
3586 I<time> an optional value used to control the printing of overdue messages. The
3587 default is now. It isn't the date of the invoice; that's the `_date' field.
3588 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3589 L<Time::Local> and L<Date::Parse> for conversion functions.
3591 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3598 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3599 my $ps = generate_ps($file);
3601 unlink($barcodefile) if $barcodefile;
3606 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3608 Returns an PDF invoice, as a scalar.
3610 Options can be passed as a hashref (recommended) or as a list of time, template
3611 and then any key/value pairs for any other options.
3613 I<time> an optional value used to control the printing of overdue messages. The
3614 default is now. It isn't the date of the invoice; that's the `_date' field.
3615 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3616 L<Time::Local> and L<Date::Parse> for conversion functions.
3618 I<template>, if specified, is the name of a suffix for alternate invoices.
3620 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3627 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3628 my $pdf = generate_pdf($file);
3630 unlink($barcodefile) if $barcodefile;
3635 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3637 Returns an HTML invoice, as a scalar.
3639 I<time> an optional value used to control the printing of overdue messages. The
3640 default is now. It isn't the date of the invoice; that's the `_date' field.
3641 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3642 L<Time::Local> and L<Date::Parse> for conversion functions.
3644 I<template>, if specified, is the name of a suffix for alternate invoices.
3646 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3648 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3649 when emailing the invoice as part of a multipart/related MIME email.
3657 %params = %{ shift() };
3659 $params{'time'} = shift;
3660 $params{'template'} = shift;
3661 $params{'cid'} = shift;
3664 $params{'format'} = 'html';
3666 $self->print_generic( %params );
3669 # quick subroutine for print_latex
3671 # There are ten characters that LaTeX treats as special characters, which
3672 # means that they do not simply typeset themselves:
3673 # # $ % & ~ _ ^ \ { }
3675 # TeX ignores blanks following an escaped character; if you want a blank (as
3676 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3680 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3681 $value =~ s/([<>])/\$$1\$/g;
3687 encode_entities($value);
3691 sub _html_escape_nbsp {
3692 my $value = _html_escape(shift);
3693 $value =~ s/ +/ /g;
3697 #utility methods for print_*
3699 sub _translate_old_latex_format {
3700 warn "_translate_old_latex_format called\n"
3707 if ( $line =~ /^%%Detail\s*$/ ) {
3709 push @template, q![@--!,
3710 q! foreach my $_tr_line (@detail_items) {!,
3711 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3712 q! $_tr_line->{'description'} .= !,
3713 q! "\\tabularnewline\n~~".!,
3714 q! join( "\\tabularnewline\n~~",!,
3715 q! @{$_tr_line->{'ext_description'}}!,
3719 while ( ( my $line_item_line = shift )
3720 !~ /^%%EndDetail\s*$/ ) {
3721 $line_item_line =~ s/'/\\'/g; # nice LTS
3722 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3723 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3724 push @template, " \$OUT .= '$line_item_line';";
3727 push @template, '}',
3730 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3732 push @template, '[@--',
3733 ' foreach my $_tr_line (@total_items) {';
3735 while ( ( my $total_item_line = shift )
3736 !~ /^%%EndTotalDetails\s*$/ ) {
3737 $total_item_line =~ s/'/\\'/g; # nice LTS
3738 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3739 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3740 push @template, " \$OUT .= '$total_item_line';";
3743 push @template, '}',
3747 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3748 push @template, $line;
3754 warn "$_\n" foreach @template;
3762 my $conf = $self->conf;
3764 #check for an invoice-specific override
3765 return $self->invoice_terms if $self->invoice_terms;
3767 #check for a customer- specific override
3768 my $cust_main = $self->cust_main;
3769 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3771 #use configured default
3772 $conf->config('invoice_default_terms') || '';
3778 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3779 $duedate = $self->_date() + ( $1 * 86400 );
3786 $self->due_date ? time2str(shift, $self->due_date) : '';
3789 sub balance_due_msg {
3791 my $msg = $self->mt('Balance Due');
3792 return $msg unless $self->terms;
3793 if ( $self->due_date ) {
3794 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3795 $self->due_date2str($date_format);
3796 } elsif ( $self->terms ) {
3797 $msg .= ' - '. $self->terms;
3802 sub balance_due_date {
3804 my $conf = $self->conf;
3806 if ( $conf->exists('invoice_default_terms')
3807 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3808 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3813 sub credit_balance_msg {
3815 $self->mt('Credit Balance Remaining')
3818 =item invnum_date_pretty
3820 Returns a string with the invoice number and date, for example:
3821 "Invoice #54 (3/20/2008)"
3825 sub invnum_date_pretty {
3827 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3832 Returns a string with the date, for example: "3/20/2008"
3838 time2str($date_format, $self->_date);
3841 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3843 Generate section information for all items appearing on this invoice.
3844 This will only be called for multi-section invoices.
3846 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3847 related display records (L<FS::cust_bill_pkg_display>) and organize
3848 them into two groups ("early" and "late" according to whether they come
3849 before or after the total), then into sections. A subtotal is calculated
3852 Section descriptions are returned in sort weight order. Each consists
3853 of a hash containing:
3855 description: the package category name, escaped
3856 subtotal: the total charges in that section
3857 tax_section: a flag indicating that the section contains only tax charges
3858 summarized: same as tax_section, for some reason
3859 sort_weight: the package category's sort weight
3861 If 'condense' is set on the display record, it also contains everything
3862 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3863 coderefs to generate parts of the invoice. This is not advised.
3867 LATE: an arrayref to push the "late" section hashes onto. The "early"
3868 group is simply returned from the method.
3870 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3871 Turning this on has the following effects:
3872 - Ignores display items with the 'summary' flag.
3873 - Combines all items into the "early" group.
3874 - Creates sections for all non-disabled package categories, even if they
3875 have no charges on this invoice, as well as a section with no name.
3877 ESCAPE: an escape function to use for section titles.
3879 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3880 sorted list. If there are any of these, section subtotals exclude
3883 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3884 passed through to C<_condense_section()>.
3888 use vars qw(%pkg_category_cache);
3889 sub _items_sections {
3892 my $summarypage = shift;
3894 my $extra_sections = shift;
3898 my %late_subtotal = ();
3901 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3904 my $usage = $cust_bill_pkg->usage;
3906 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3907 next if ( $display->summary && $summarypage );
3909 my $section = $display->section;
3910 my $type = $display->type;
3912 $not_tax{$section} = 1
3913 unless $cust_bill_pkg->pkgnum == 0;
3915 if ( $display->post_total && !$summarypage ) {
3916 if (! $type || $type eq 'S') {
3917 $late_subtotal{$section} += $cust_bill_pkg->setup
3918 if $cust_bill_pkg->setup != 0
3919 || $cust_bill_pkg->setup_show_zero;
3923 $late_subtotal{$section} += $cust_bill_pkg->recur
3924 if $cust_bill_pkg->recur != 0
3925 || $cust_bill_pkg->recur_show_zero;
3928 if ($type && $type eq 'R') {
3929 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3930 if $cust_bill_pkg->recur != 0
3931 || $cust_bill_pkg->recur_show_zero;
3934 if ($type && $type eq 'U') {
3935 $late_subtotal{$section} += $usage
3936 unless scalar(@$extra_sections);
3941 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3943 if (! $type || $type eq 'S') {
3944 $subtotal{$section} += $cust_bill_pkg->setup
3945 if $cust_bill_pkg->setup != 0
3946 || $cust_bill_pkg->setup_show_zero;
3950 $subtotal{$section} += $cust_bill_pkg->recur
3951 if $cust_bill_pkg->recur != 0
3952 || $cust_bill_pkg->recur_show_zero;
3955 if ($type && $type eq 'R') {
3956 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3957 if $cust_bill_pkg->recur != 0
3958 || $cust_bill_pkg->recur_show_zero;
3961 if ($type && $type eq 'U') {
3962 $subtotal{$section} += $usage
3963 unless scalar(@$extra_sections);
3972 %pkg_category_cache = ();
3974 push @$late, map { { 'description' => &{$escape}($_),
3975 'subtotal' => $late_subtotal{$_},
3977 'sort_weight' => ( _pkg_category($_)
3978 ? _pkg_category($_)->weight
3981 ((_pkg_category($_) && _pkg_category($_)->condense)
3982 ? $self->_condense_section($format)
3986 sort _sectionsort keys %late_subtotal;
3989 if ( $summarypage ) {
3990 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3991 map { $_->categoryname } qsearch('pkg_category', {});
3992 push @sections, '' if exists($subtotal{''});
3994 @sections = keys %subtotal;
3997 my @early = map { { 'description' => &{$escape}($_),
3998 'subtotal' => $subtotal{$_},
3999 'summarized' => $not_tax{$_} ? '' : 'Y',
4000 'tax_section' => $not_tax{$_} ? '' : 'Y',
4001 'sort_weight' => ( _pkg_category($_)
4002 ? _pkg_category($_)->weight
4005 ((_pkg_category($_) && _pkg_category($_)->condense)
4006 ? $self->_condense_section($format)
4011 push @early, @$extra_sections if $extra_sections;
4013 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4017 #helper subs for above
4020 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4024 my $categoryname = shift;
4025 $pkg_category_cache{$categoryname} ||=
4026 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4029 my %condensed_format = (
4030 'label' => [ qw( Description Qty Amount ) ],
4032 sub { shift->{description} },
4033 sub { shift->{quantity} },
4034 sub { my($href, %opt) = @_;
4035 ($opt{dollar} || ''). $href->{amount};
4038 'align' => [ qw( l r r ) ],
4039 'span' => [ qw( 5 1 1 ) ], # unitprices?
4040 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4043 sub _condense_section {
4044 my ( $self, $format ) = ( shift, shift );
4046 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4047 qw( description_generator
4050 total_line_generator
4055 sub _condensed_generator_defaults {
4056 my ( $self, $format ) = ( shift, shift );
4057 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4066 sub _condensed_header_generator {
4067 my ( $self, $format ) = ( shift, shift );
4069 my ( $f, $prefix, $suffix, $separator, $column ) =
4070 _condensed_generator_defaults($format);
4072 if ($format eq 'latex') {
4073 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4074 $suffix = "\\\\\n\\hline";
4077 sub { my ($d,$a,$s,$w) = @_;
4078 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4080 } elsif ( $format eq 'html' ) {
4081 $prefix = '<th></th>';
4085 sub { my ($d,$a,$s,$w) = @_;
4086 return qq!<th align="$html_align{$a}">$d</th>!;
4094 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4096 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4099 $prefix. join($separator, @result). $suffix;
4104 sub _condensed_description_generator {
4105 my ( $self, $format ) = ( shift, shift );
4107 my ( $f, $prefix, $suffix, $separator, $column ) =
4108 _condensed_generator_defaults($format);
4110 my $money_char = '$';
4111 if ($format eq 'latex') {
4112 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4114 $separator = " & \n";
4116 sub { my ($d,$a,$s,$w) = @_;
4117 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4119 $money_char = '\\dollar';
4120 }elsif ( $format eq 'html' ) {
4121 $prefix = '"><td align="center"></td>';
4125 sub { my ($d,$a,$s,$w) = @_;
4126 return qq!<td align="$html_align{$a}">$d</td>!;
4128 #$money_char = $conf->config('money_char') || '$';
4129 $money_char = ''; # this is madness
4137 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4139 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4141 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4142 map { $f->{$_}->[$i] } qw(align span width)
4146 $prefix. join( $separator, @result ). $suffix;
4151 sub _condensed_total_generator {
4152 my ( $self, $format ) = ( shift, shift );
4154 my ( $f, $prefix, $suffix, $separator, $column ) =
4155 _condensed_generator_defaults($format);
4158 if ($format eq 'latex') {
4161 $separator = " & \n";
4163 sub { my ($d,$a,$s,$w) = @_;
4164 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4166 }elsif ( $format eq 'html' ) {
4170 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4172 sub { my ($d,$a,$s,$w) = @_;
4173 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4182 # my $r = &{$f->{fields}->[$i]}(@args);
4183 # $r .= ' Total' unless $i;
4185 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4187 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4188 map { $f->{$_}->[$i] } qw(align span width)
4192 $prefix. join( $separator, @result ). $suffix;
4197 =item total_line_generator FORMAT
4199 Returns a coderef used for generation of invoice total line items for this
4200 usage_class. FORMAT is either html or latex
4204 # should not be used: will have issues with hash element names (description vs
4205 # total_item and amount vs total_amount -- another array of functions?
4207 sub _condensed_total_line_generator {
4208 my ( $self, $format ) = ( shift, shift );
4210 my ( $f, $prefix, $suffix, $separator, $column ) =
4211 _condensed_generator_defaults($format);
4214 if ($format eq 'latex') {
4217 $separator = " & \n";
4219 sub { my ($d,$a,$s,$w) = @_;
4220 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4222 }elsif ( $format eq 'html' ) {
4226 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4228 sub { my ($d,$a,$s,$w) = @_;
4229 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4238 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4240 &{$column}( &{$f->{fields}->[$i]}(@args),
4241 map { $f->{$_}->[$i] } qw(align span width)
4245 $prefix. join( $separator, @result ). $suffix;
4250 #sub _items_extra_usage_sections {
4252 # my $escape = shift;
4254 # my %sections = ();
4256 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4257 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4259 # next unless $cust_bill_pkg->pkgnum > 0;
4261 # foreach my $section ( keys %usage_class ) {
4263 # my $usage = $cust_bill_pkg->usage($section);
4265 # next unless $usage && $usage > 0;
4267 # $sections{$section} ||= 0;
4268 # $sections{$section} += $usage;
4274 # map { { 'description' => &{$escape}($_),
4275 # 'subtotal' => $sections{$_},
4276 # 'summarized' => '',
4277 # 'tax_section' => '',
4280 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4284 sub _items_extra_usage_sections {
4286 my $conf = $self->conf;
4294 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4296 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4297 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4298 next unless $cust_bill_pkg->pkgnum > 0;
4300 foreach my $classnum ( keys %usage_class ) {
4301 my $section = $usage_class{$classnum}->classname;
4302 $classnums{$section} = $classnum;
4304 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4305 my $amount = $detail->amount;
4306 next unless $amount && $amount > 0;
4308 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4309 $sections{$section}{amount} += $amount; #subtotal
4310 $sections{$section}{calls}++;
4311 $sections{$section}{duration} += $detail->duration;
4313 my $desc = $detail->regionname;
4314 my $description = $desc;
4315 $description = substr($desc, 0, $maxlength). '...'
4316 if $format eq 'latex' && length($desc) > $maxlength;
4318 $lines{$section}{$desc} ||= {
4319 description => &{$escape}($description),
4320 #pkgpart => $part_pkg->pkgpart,
4321 pkgnum => $cust_bill_pkg->pkgnum,
4326 #unit_amount => $cust_bill_pkg->unitrecur,
4327 quantity => $cust_bill_pkg->quantity,
4328 product_code => 'N/A',
4329 ext_description => [],
4332 $lines{$section}{$desc}{amount} += $amount;
4333 $lines{$section}{$desc}{calls}++;
4334 $lines{$section}{$desc}{duration} += $detail->duration;
4340 my %sectionmap = ();
4341 foreach (keys %sections) {
4342 my $usage_class = $usage_class{$classnums{$_}};
4343 $sectionmap{$_} = { 'description' => &{$escape}($_),
4344 'amount' => $sections{$_}{amount}, #subtotal
4345 'calls' => $sections{$_}{calls},
4346 'duration' => $sections{$_}{duration},
4348 'tax_section' => '',
4349 'sort_weight' => $usage_class->weight,
4350 ( $usage_class->format
4351 ? ( map { $_ => $usage_class->$_($format) }
4352 qw( description_generator header_generator total_generator total_line_generator )
4359 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4363 foreach my $section ( keys %lines ) {
4364 foreach my $line ( keys %{$lines{$section}} ) {
4365 my $l = $lines{$section}{$line};
4366 $l->{section} = $sectionmap{$section};
4367 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4368 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4373 return(\@sections, \@lines);
4379 my $end = $self->_date;
4381 # start at date of previous invoice + 1 second or 0 if no previous invoice
4382 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4383 $start = 0 if !$start;
4386 my $cust_main = $self->cust_main;
4387 my @pkgs = $cust_main->all_pkgs;
4388 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4391 foreach my $pkg ( @pkgs ) {
4392 my @h_cust_svc = $pkg->h_cust_svc($end);
4393 foreach my $h_cust_svc ( @h_cust_svc ) {
4394 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4395 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4397 my $inserted = $h_cust_svc->date_inserted;
4398 my $deleted = $h_cust_svc->date_deleted;
4399 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4401 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4403 # DID either activated or ported in; cannot be both for same DID simultaneously
4404 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4405 && (!$phone_inserted->lnp_status
4406 || $phone_inserted->lnp_status eq ''
4407 || $phone_inserted->lnp_status eq 'native')) {
4410 else { # this one not so clean, should probably move to (h_)svc_phone
4411 my $phone_portedin = qsearchs( 'h_svc_phone',
4412 { 'svcnum' => $h_cust_svc->svcnum,
4413 'lnp_status' => 'portedin' },
4414 FS::h_svc_phone->sql_h_searchs($end),
4416 $num_portedin++ if $phone_portedin;
4419 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4420 if($deleted >= $start && $deleted <= $end && $phone_deleted
4421 && (!$phone_deleted->lnp_status
4422 || $phone_deleted->lnp_status ne 'portingout')) {
4425 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4426 && $phone_deleted->lnp_status
4427 && $phone_deleted->lnp_status eq 'portingout') {
4431 # increment usage minutes
4432 if ( $phone_inserted ) {
4433 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4434 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4437 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4440 # don't look at this service again
4441 push @seen, $h_cust_svc->svcnum;
4445 $minutes = sprintf("%d", $minutes);
4446 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4447 . "$num_deactivated Ported-Out: $num_portedout ",
4448 "Total Minutes: $minutes");
4451 sub _items_accountcode_cdr {
4456 my $section = { 'amount' => 0,
4459 'sort_weight' => '',
4461 'description' => 'Usage by Account Code',
4467 my %accountcodes = ();
4469 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4470 next unless $cust_bill_pkg->pkgnum > 0;
4472 my @header = $cust_bill_pkg->details_header;
4473 next unless scalar(@header);
4474 $section->{'header'} = join(',',@header);
4476 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4478 $section->{'header'} = $detail->formatted('format' => $format)
4479 if($detail->detail eq $section->{'header'});
4481 my $accountcode = $detail->accountcode;
4482 next unless $accountcode;
4484 my $amount = $detail->amount;
4485 next unless $amount && $amount > 0;
4487 $accountcodes{$accountcode} ||= {
4488 description => $accountcode,
4495 product_code => 'N/A',
4496 section => $section,
4497 ext_description => [ $section->{'header'} ],
4501 $section->{'amount'} += $amount;
4502 $accountcodes{$accountcode}{'amount'} += $amount;
4503 $accountcodes{$accountcode}{calls}++;
4504 $accountcodes{$accountcode}{duration} += $detail->duration;
4505 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4509 foreach my $l ( values %accountcodes ) {
4510 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4511 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4512 foreach my $sorted_detail ( @sorted_detail ) {
4513 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4515 delete $l->{detail_temp};
4519 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4521 return ($section,\@sorted_lines);
4524 sub _items_svc_phone_sections {
4526 my $conf = $self->conf;
4534 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4536 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4537 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4539 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4540 next unless $cust_bill_pkg->pkgnum > 0;
4542 my @header = $cust_bill_pkg->details_header;
4543 next unless scalar(@header);
4545 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4547 my $phonenum = $detail->phonenum;
4548 next unless $phonenum;
4550 my $amount = $detail->amount;
4551 next unless $amount && $amount > 0;
4553 $sections{$phonenum} ||= { 'amount' => 0,
4556 'sort_weight' => -1,
4557 'phonenum' => $phonenum,
4559 $sections{$phonenum}{amount} += $amount; #subtotal
4560 $sections{$phonenum}{calls}++;
4561 $sections{$phonenum}{duration} += $detail->duration;
4563 my $desc = $detail->regionname;
4564 my $description = $desc;
4565 $description = substr($desc, 0, $maxlength). '...'
4566 if $format eq 'latex' && length($desc) > $maxlength;
4568 $lines{$phonenum}{$desc} ||= {
4569 description => &{$escape}($description),
4570 #pkgpart => $part_pkg->pkgpart,
4578 product_code => 'N/A',
4579 ext_description => [],
4582 $lines{$phonenum}{$desc}{amount} += $amount;
4583 $lines{$phonenum}{$desc}{calls}++;
4584 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4586 my $line = $usage_class{$detail->classnum}->classname;
4587 $sections{"$phonenum $line"} ||=
4591 'sort_weight' => $usage_class{$detail->classnum}->weight,
4592 'phonenum' => $phonenum,
4593 'header' => [ @header ],
4595 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4596 $sections{"$phonenum $line"}{calls}++;
4597 $sections{"$phonenum $line"}{duration} += $detail->duration;
4599 $lines{"$phonenum $line"}{$desc} ||= {
4600 description => &{$escape}($description),
4601 #pkgpart => $part_pkg->pkgpart,
4609 product_code => 'N/A',
4610 ext_description => [],
4613 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4614 $lines{"$phonenum $line"}{$desc}{calls}++;
4615 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4616 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4617 $detail->formatted('format' => $format);
4622 my %sectionmap = ();
4623 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4624 foreach ( keys %sections ) {
4625 my @header = @{ $sections{$_}{header} || [] };
4627 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4628 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4629 my $usage_class = $summary ? $simple : $usage_simple;
4630 my $ending = $summary ? ' usage charges' : '';
4633 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4635 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4636 'amount' => $sections{$_}{amount}, #subtotal
4637 'calls' => $sections{$_}{calls},
4638 'duration' => $sections{$_}{duration},
4640 'tax_section' => '',
4641 'phonenum' => $sections{$_}{phonenum},
4642 'sort_weight' => $sections{$_}{sort_weight},
4643 'post_total' => $summary, #inspire pagebreak
4645 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4646 qw( description_generator
4649 total_line_generator
4656 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4657 $a->{sort_weight} <=> $b->{sort_weight}
4662 foreach my $section ( keys %lines ) {
4663 foreach my $line ( keys %{$lines{$section}} ) {
4664 my $l = $lines{$section}{$line};
4665 $l->{section} = $sectionmap{$section};
4666 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4667 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4672 if($conf->exists('phone_usage_class_summary')) {
4673 # this only works with Latex
4677 # after this, we'll have only two sections per DID:
4678 # Calls Summary and Calls Detail
4679 foreach my $section ( @sections ) {
4680 if($section->{'post_total'}) {
4681 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4682 $section->{'total_line_generator'} = sub { '' };
4683 $section->{'total_generator'} = sub { '' };
4684 $section->{'header_generator'} = sub { '' };
4685 $section->{'description_generator'} = '';
4686 push @newsections, $section;
4687 my %calls_detail = %$section;
4688 $calls_detail{'post_total'} = '';
4689 $calls_detail{'sort_weight'} = '';
4690 $calls_detail{'description_generator'} = sub { '' };
4691 $calls_detail{'header_generator'} = sub {
4692 return ' & Date/Time & Called Number & Duration & Price'
4693 if $format eq 'latex';
4696 $calls_detail{'description'} = 'Calls Detail: '
4697 . $section->{'phonenum'};
4698 push @newsections, \%calls_detail;
4702 # after this, each usage class is collapsed/summarized into a single
4703 # line under the Calls Summary section
4704 foreach my $newsection ( @newsections ) {
4705 if($newsection->{'post_total'}) { # this means Calls Summary
4706 foreach my $section ( @sections ) {
4707 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4708 && !$section->{'post_total'});
4709 my $newdesc = $section->{'description'};
4710 my $tn = $section->{'phonenum'};
4711 $newdesc =~ s/$tn//g;
4712 my $line = { ext_description => [],
4716 calls => $section->{'calls'},
4717 section => $newsection,
4718 duration => $section->{'duration'},
4719 description => $newdesc,
4720 amount => sprintf("%.2f",$section->{'amount'}),
4721 product_code => 'N/A',
4723 push @newlines, $line;
4728 # after this, Calls Details is populated with all CDRs
4729 foreach my $newsection ( @newsections ) {
4730 if(!$newsection->{'post_total'}) { # this means Calls Details
4731 foreach my $line ( @lines ) {
4732 next unless (scalar(@{$line->{'ext_description'}}) &&
4733 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4735 my @extdesc = @{$line->{'ext_description'}};
4737 foreach my $extdesc ( @extdesc ) {
4738 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4739 push @newextdesc, $extdesc;
4741 $line->{'ext_description'} = \@newextdesc;
4742 $line->{'section'} = $newsection;
4743 push @newlines, $line;
4748 return(\@newsections, \@newlines);
4751 return(\@sections, \@lines);
4755 sub _items { # seems to be unused
4758 #my @display = scalar(@_)
4760 # : qw( _items_previous _items_pkg );
4761 # #: qw( _items_pkg );
4762 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4763 my @display = qw( _items_previous _items_pkg );
4766 foreach my $display ( @display ) {
4767 push @b, $self->$display(@_);
4772 sub _items_previous {
4774 my $conf = $self->conf;
4775 my $cust_main = $self->cust_main;
4776 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4778 foreach ( @pr_cust_bill ) {
4779 my $date = $conf->exists('invoice_show_prior_due_date')
4780 ? 'due '. $_->due_date2str($date_format)
4781 : time2str($date_format, $_->_date);
4783 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4784 #'pkgpart' => 'N/A',
4786 'amount' => sprintf("%.2f", $_->owed),
4792 # 'description' => 'Previous Balance',
4793 # #'pkgpart' => 'N/A',
4794 # 'pkgnum' => 'N/A',
4795 # 'amount' => sprintf("%10.2f", $pr_total ),
4796 # 'ext_description' => [ map {
4797 # "Invoice ". $_->invnum.
4798 # " (". time2str("%x",$_->_date). ") ".
4799 # sprintf("%10.2f", $_->owed)
4800 # } @pr_cust_bill ],
4805 =item _items_pkg [ OPTIONS ]
4807 Return line item hashes for each package item on this invoice. Nearly
4810 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4812 The only OPTIONS accepted is 'section', which may point to a hashref
4813 with a key named 'condensed', which may have a true value. If it
4814 does, this method tries to merge identical items into items with
4815 'quantity' equal to the number of items (not the sum of their
4816 separate quantities, for some reason).
4824 warn "$me _items_pkg searching for all package line items\n"
4827 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4829 warn "$me _items_pkg filtering line items\n"
4831 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4833 if ($options{section} && $options{section}->{condensed}) {
4835 warn "$me _items_pkg condensing section\n"
4839 local $Storable::canonical = 1;
4840 foreach ( @items ) {
4842 delete $item->{ref};
4843 delete $item->{ext_description};
4844 my $key = freeze($item);
4845 $itemshash{$key} ||= 0;
4846 $itemshash{$key} ++; # += $item->{quantity};
4848 @items = sort { $a->{description} cmp $b->{description} }
4849 map { my $i = thaw($_);
4850 $i->{quantity} = $itemshash{$_};
4852 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4858 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4865 return 0 unless $a->itemdesc cmp $b->itemdesc;
4866 return -1 if $b->itemdesc eq 'Tax';
4867 return 1 if $a->itemdesc eq 'Tax';
4868 return -1 if $b->itemdesc eq 'Other surcharges';
4869 return 1 if $a->itemdesc eq 'Other surcharges';
4870 $a->itemdesc cmp $b->itemdesc;
4875 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4876 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4879 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4881 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4882 list of hashrefs describing the line items they generate on the invoice.
4884 OPTIONS may include:
4886 format: the invoice format.
4888 escape_function: the function used to escape strings.
4890 DEPRECATED? (expensive, mostly unused?)
4891 format_function: the function used to format CDRs.
4893 section: a hashref containing 'description'; if this is present,
4894 cust_bill_pkg_display records not belonging to this section are
4897 multisection: a flag indicating that this is a multisection invoice,
4898 which does something complicated.
4900 multilocation: a flag to display the location label for the package.
4902 Returns a list of hashrefs, each of which may contain:
4904 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4905 ext_description, which is an arrayref of detail lines to show below
4910 sub _items_cust_bill_pkg {
4912 my $conf = $self->conf;
4913 my $cust_bill_pkgs = shift;
4916 my $format = $opt{format} || '';
4917 my $escape_function = $opt{escape_function} || sub { shift };
4918 my $format_function = $opt{format_function} || '';
4919 my $no_usage = $opt{no_usage} || '';
4920 my $unsquelched = $opt{unsquelched} || ''; #unused
4921 my $section = $opt{section}->{description} if $opt{section};
4922 my $summary_page = $opt{summary_page} || ''; #unused
4923 my $multilocation = $opt{multilocation} || '';
4924 my $multisection = $opt{multisection} || '';
4925 my $discount_show_always = 0;
4927 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4929 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4932 my ($s, $r, $u) = ( undef, undef, undef );
4933 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4936 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4937 if ( $_ && !$cust_bill_pkg->hidden ) {
4938 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4939 $_->{amount} =~ s/^\-0\.00$/0.00/;
4940 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4942 if $_->{amount} != 0
4943 || $discount_show_always
4944 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4945 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4951 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4953 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4954 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4957 foreach my $display ( grep { defined($section)
4958 ? $_->section eq $section
4961 #grep { !$_->summary || !$summary_page } # bunk!
4962 grep { !$_->summary || $multisection }
4963 @cust_bill_pkg_display
4967 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4968 $display->billpkgdisplaynum. "\n"
4971 my $type = $display->type;
4973 my $desc = $cust_bill_pkg->desc;
4974 $desc = substr($desc, 0, $maxlength). '...'
4975 if $format eq 'latex' && length($desc) > $maxlength;
4977 my %details_opt = ( 'format' => $format,
4978 'escape_function' => $escape_function,
4979 'format_function' => $format_function,
4980 'no_usage' => $opt{'no_usage'},
4983 if ( $cust_bill_pkg->pkgnum > 0 ) {
4985 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4988 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4990 # start/end dates for invoice formats that do nonstandard
4992 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4994 if ( (!$type || $type eq 'S')
4995 && ( $cust_bill_pkg->setup != 0
4996 || $cust_bill_pkg->setup_show_zero
5001 warn "$me _items_cust_bill_pkg adding setup\n"
5004 my $description = $desc;
5005 $description .= ' Setup'
5006 if $cust_bill_pkg->recur != 0
5007 || $discount_show_always
5008 || $cust_bill_pkg->recur_show_zero;
5011 unless ( $cust_pkg->part_pkg->hide_svc_detail
5012 || $cust_bill_pkg->hidden )
5015 push @d, map &{$escape_function}($_),
5016 $cust_pkg->h_labels_short($self->_date, undef, 'I')
5017 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5019 if ( $multilocation ) {
5020 my $loc = $cust_pkg->location_label;
5021 $loc = substr($loc, 0, $maxlength). '...'
5022 if $format eq 'latex' && length($loc) > $maxlength;
5023 push @d, &{$escape_function}($loc);
5026 } #unless hiding service details
5028 push @d, $cust_bill_pkg->details(%details_opt)
5029 if $cust_bill_pkg->recur == 0;
5031 if ( $cust_bill_pkg->hidden ) {
5032 $s->{amount} += $cust_bill_pkg->setup;
5033 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5034 push @{ $s->{ext_description} }, @d;
5038 description => $description,
5039 #pkgpart => $part_pkg->pkgpart,
5040 pkgnum => $cust_bill_pkg->pkgnum,
5041 amount => $cust_bill_pkg->setup,
5042 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5043 unit_amount => $cust_bill_pkg->unitsetup,
5044 quantity => $cust_bill_pkg->quantity,
5045 ext_description => \@d,
5051 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5053 $cust_bill_pkg->recur != 0
5054 || $cust_bill_pkg->setup == 0
5055 || $discount_show_always
5056 || $cust_bill_pkg->recur_show_zero
5061 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5064 my $is_summary = $display->summary;
5065 my $description = ($is_summary && $type && $type eq 'U')
5066 ? "Usage charges" : $desc;
5068 #pry be a bit more efficient to look some of this conf stuff up
5071 $conf->exists('disable_line_item_date_ranges')
5072 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5075 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5076 $cust_main->agentnum
5078 if ( defined($date_style) && $date_style eq 'month_of' ) {
5079 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5080 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5081 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5082 $cust_main->agentnum
5084 $desc .= ' ' unless $desc =~ /\s$/;
5085 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5087 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5088 " - ". time2str($date_format, $cust_bill_pkg->edate);
5090 $description .= " ($time_period)";
5094 my @seconds = (); # for display of usage info
5096 #at least until cust_bill_pkg has "past" ranges in addition to
5097 #the "future" sdate/edate ones... see #3032
5098 my @dates = ( $self->_date );
5099 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5100 push @dates, $prev->sdate if $prev;
5101 push @dates, undef if !$prev;
5103 unless ( $cust_pkg->part_pkg->hide_svc_detail
5104 || $cust_bill_pkg->itemdesc
5105 || $cust_bill_pkg->hidden
5106 || $is_summary && $type && $type eq 'U' )
5109 warn "$me _items_cust_bill_pkg adding service details\n"
5112 push @d, map &{$escape_function}($_),
5113 $cust_pkg->h_labels_short(@dates, 'I')
5114 #$cust_bill_pkg->edate,
5115 #$cust_bill_pkg->sdate)
5116 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5118 warn "$me _items_cust_bill_pkg done adding service details\n"
5121 if ( $multilocation ) {
5122 my $loc = $cust_pkg->location_label;
5123 $loc = substr($loc, 0, $maxlength). '...'
5124 if $format eq 'latex' && length($loc) > $maxlength;
5125 push @d, &{$escape_function}($loc);
5128 # Display of seconds_since_sqlradacct:
5129 # On the invoice, when processing @detail_items, look for a field
5130 # named 'seconds'. This will contain total seconds for each
5131 # service, in the same order as @ext_description. For services
5132 # that don't support this it will show undef.
5133 if ( $conf->exists('svc_acct-usage_seconds')
5134 and ! $cust_bill_pkg->pkgpart_override ) {
5135 foreach my $cust_svc (
5136 $cust_pkg->h_cust_svc(@dates, 'I')
5139 # eval because not having any part_export_usage exports
5140 # is a fatal error, last_bill/_date because that's how
5141 # sqlradius_hour billing does it
5143 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5145 push @seconds, $sec;
5147 } #if svc_acct-usage_seconds
5151 unless ( $is_summary ) {
5152 warn "$me _items_cust_bill_pkg adding details\n"
5155 #instead of omitting details entirely in this case (unwanted side
5156 # effects), just omit CDRs
5157 $details_opt{'no_usage'} = 1
5158 if $type && $type eq 'R';
5160 push @d, $cust_bill_pkg->details(%details_opt);
5163 warn "$me _items_cust_bill_pkg calculating amount\n"
5168 $amount = $cust_bill_pkg->recur;
5169 } elsif ($type eq 'R') {
5170 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5171 } elsif ($type eq 'U') {
5172 $amount = $cust_bill_pkg->usage;
5175 if ( !$type || $type eq 'R' ) {
5177 warn "$me _items_cust_bill_pkg adding recur\n"
5180 if ( $cust_bill_pkg->hidden ) {
5181 $r->{amount} += $amount;
5182 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5183 push @{ $r->{ext_description} }, @d;
5186 description => $description,
5187 #pkgpart => $part_pkg->pkgpart,
5188 pkgnum => $cust_bill_pkg->pkgnum,
5190 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5191 unit_amount => $cust_bill_pkg->unitrecur,
5192 quantity => $cust_bill_pkg->quantity,
5194 ext_description => \@d,
5196 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5199 } else { # $type eq 'U'
5201 warn "$me _items_cust_bill_pkg adding usage\n"
5204 if ( $cust_bill_pkg->hidden ) {
5205 $u->{amount} += $amount;
5206 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5207 push @{ $u->{ext_description} }, @d;
5210 description => $description,
5211 #pkgpart => $part_pkg->pkgpart,
5212 pkgnum => $cust_bill_pkg->pkgnum,
5214 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5215 unit_amount => $cust_bill_pkg->unitrecur,
5216 quantity => $cust_bill_pkg->quantity,
5218 ext_description => \@d,
5223 } # recurring or usage with recurring charge
5225 } else { #pkgnum tax or one-shot line item (??)
5227 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5230 if ( $cust_bill_pkg->setup != 0 ) {
5232 'description' => $desc,
5233 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5236 if ( $cust_bill_pkg->recur != 0 ) {
5238 'description' => "$desc (".
5239 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5240 time2str($date_format, $cust_bill_pkg->edate). ')',
5241 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5249 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5250 && $conf->exists('discount-show-always'));
5254 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5256 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5257 $_->{amount} =~ s/^\-0\.00$/0.00/;
5258 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5260 if $_->{amount} != 0
5261 || $discount_show_always
5262 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5263 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5267 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5274 sub _items_credits {
5275 my( $self, %opt ) = @_;
5276 my $trim_len = $opt{'trim_len'} || 60;
5280 foreach ( $self->cust_credited ) {
5282 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5284 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5285 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5286 $reason = " ($reason) " if $reason;
5289 #'description' => 'Credit ref\#'. $_->crednum.
5290 # " (". time2str("%x",$_->cust_credit->_date) .")".
5292 'description' => $self->mt('Credit applied').' '.
5293 time2str($date_format,$_->cust_credit->_date). $reason,
5294 'amount' => sprintf("%.2f",$_->amount),
5302 sub _items_payments {
5306 #get & print payments
5307 foreach ( $self->cust_bill_pay ) {
5309 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5312 'description' => $self->mt('Payment received').' '.
5313 time2str($date_format,$_->cust_pay->_date ),
5314 'amount' => sprintf("%.2f", $_->amount )
5322 =item _items_discounts_avail
5324 Returns an array of line item hashrefs representing available term discounts
5325 for this invoice. This makes the same assumptions that apply to term
5326 discounts in general: that the package is billed monthly, at a flat rate,
5327 with no usage charges. A prorated first month will be handled, as will
5328 a setup fee if the discount is allowed to apply to setup fees.
5332 sub _items_discounts_avail {
5334 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5336 my %plans = $self->discount_plans;
5338 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5342 my $plan = $plans{$months};
5344 my $term_total = sprintf('%.2f', $plan->discounted_total);
5345 my $percent = sprintf('%.0f',
5346 100 * (1 - $term_total / $plan->base_total) );
5347 my $permonth = sprintf('%.2f', $term_total / $months);
5348 my $detail = $self->mt('discount on item'). ' '.
5349 join(', ', map { "#$_" } $plan->pkgnums)
5352 # discounts for non-integer months don't work anyway
5353 $months = sprintf("%d", $months);
5356 description => $self->mt('Save [_1]% by paying for [_2] months',
5358 amount => $self->mt('[_1] ([_2] per month)',
5359 $term_total, $money_char.$permonth),
5360 ext_description => ($detail || ''),
5363 sort { $b <=> $a } keys %plans;
5367 =item call_details [ OPTION => VALUE ... ]
5369 Returns an array of CSV strings representing the call details for this invoice
5370 The only option available is the boolean prepend_billed_number
5375 my ($self, %opt) = @_;
5377 my $format_function = sub { shift };
5379 if ($opt{prepend_billed_number}) {
5380 $format_function = sub {
5384 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5389 my @details = map { $_->details( 'format_function' => $format_function,
5390 'escape_function' => sub{ return() },
5394 $self->cust_bill_pkg;
5395 my $header = $details[0];
5396 ( $header, grep { $_ ne $header } @details );
5406 =item process_reprint
5410 sub process_reprint {
5411 process_re_X('print', @_);
5414 =item process_reemail
5418 sub process_reemail {
5419 process_re_X('email', @_);
5427 process_re_X('fax', @_);
5435 process_re_X('ftp', @_);
5442 sub process_respool {
5443 process_re_X('spool', @_);
5446 use Storable qw(thaw);
5450 my( $method, $job ) = ( shift, shift );
5451 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5453 my $param = thaw(decode_base64(shift));
5454 warn Dumper($param) if $DEBUG;
5465 my($method, $job, %param ) = @_;
5467 warn "re_X $method for job $job with param:\n".
5468 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5471 #some false laziness w/search/cust_bill.html
5473 my $orderby = 'ORDER BY cust_bill._date';
5475 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5477 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5479 my @cust_bill = qsearch( {
5480 #'select' => "cust_bill.*",
5481 'table' => 'cust_bill',
5482 'addl_from' => $addl_from,
5484 'extra_sql' => $extra_sql,
5485 'order_by' => $orderby,
5489 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5491 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5494 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5495 foreach my $cust_bill ( @cust_bill ) {
5496 $cust_bill->$method();
5498 if ( $job ) { #progressbar foo
5500 if ( time - $min_sec > $last ) {
5501 my $error = $job->update_statustext(
5502 int( 100 * $num / scalar(@cust_bill) )
5504 die $error if $error;
5515 =head1 CLASS METHODS
5521 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5526 my ($class, $start, $end) = @_;
5528 $class->paid_sql($start, $end). ' - '.
5529 $class->credited_sql($start, $end);
5534 Returns an SQL fragment to retreive the net amount (charged minus credited).
5539 my ($class, $start, $end) = @_;
5540 'charged - '. $class->credited_sql($start, $end);
5545 Returns an SQL fragment to retreive the amount paid against this invoice.
5550 my ($class, $start, $end) = @_;
5551 $start &&= "AND cust_bill_pay._date <= $start";
5552 $end &&= "AND cust_bill_pay._date > $end";
5553 $start = '' unless defined($start);
5554 $end = '' unless defined($end);
5555 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5556 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5561 Returns an SQL fragment to retreive the amount credited against this invoice.
5566 my ($class, $start, $end) = @_;
5567 $start &&= "AND cust_credit_bill._date <= $start";
5568 $end &&= "AND cust_credit_bill._date > $end";
5569 $start = '' unless defined($start);
5570 $end = '' unless defined($end);
5571 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5572 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5577 Returns an SQL fragment to retrieve the due date of an invoice.
5578 Currently only supported on PostgreSQL.
5583 my $conf = new FS::Conf;
5587 cust_bill.invoice_terms,
5588 cust_main.invoice_terms,
5589 \''.($conf->config('invoice_default_terms') || '').'\'
5590 ), E\'Net (\\\\d+)\'
5592 ) * 86400 + cust_bill._date'
5595 =item search_sql_where HASHREF
5597 Class method which returns an SQL WHERE fragment to search for parameters
5598 specified in HASHREF. Valid parameters are
5604 List reference of start date, end date, as UNIX timestamps.
5614 List reference of charged limits (exclusive).
5618 List reference of charged limits (exclusive).
5622 flag, return open invoices only
5626 flag, return net invoices only
5630 =item newest_percust
5634 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5638 sub search_sql_where {
5639 my($class, $param) = @_;
5641 warn "$me search_sql_where called with params: \n".
5642 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5648 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5649 push @search, "cust_main.agentnum = $1";
5653 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5654 push @search, "cust_main.refnum = $1";
5658 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5659 push @search, "cust_bill.custnum = $1";
5663 if ( $param->{_date} ) {
5664 my($beginning, $ending) = @{$param->{_date}};
5666 push @search, "cust_bill._date >= $beginning",
5667 "cust_bill._date < $ending";
5671 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5672 push @search, "cust_bill.invnum >= $1";
5674 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5675 push @search, "cust_bill.invnum <= $1";
5679 if ( $param->{charged} ) {
5680 my @charged = ref($param->{charged})
5681 ? @{ $param->{charged} }
5682 : ($param->{charged});
5684 push @search, map { s/^charged/cust_bill.charged/; $_; }
5688 my $owed_sql = FS::cust_bill->owed_sql;
5691 if ( $param->{owed} ) {
5692 my @owed = ref($param->{owed})
5693 ? @{ $param->{owed} }
5695 push @search, map { s/^owed/$owed_sql/; $_; }
5700 push @search, "0 != $owed_sql"
5701 if $param->{'open'};
5702 push @search, '0 != '. FS::cust_bill->net_sql
5706 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5707 if $param->{'days'};
5710 if ( $param->{'newest_percust'} ) {
5712 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5713 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5715 my @newest_where = map { my $x = $_;
5716 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5719 grep ! /^cust_main./, @search;
5720 my $newest_where = scalar(@newest_where)
5721 ? ' AND '. join(' AND ', @newest_where)
5725 push @search, "cust_bill._date = (
5726 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5727 WHERE newest_cust_bill.custnum = cust_bill.custnum
5733 #promised_date - also has an option to accept nulls
5734 if ( $param->{promised_date} ) {
5735 my($beginning, $ending, $null) = @{$param->{promised_date}};
5737 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5738 "cust_bill.promised_date < $ending )" .
5739 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5742 #agent virtualization
5743 my $curuser = $FS::CurrentUser::CurrentUser;
5744 if ( $curuser->username eq 'fs_queue'
5745 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5747 my $newuser = qsearchs('access_user', {
5748 'username' => $username,
5752 $curuser = $newuser;
5754 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5757 push @search, $curuser->agentnums_sql;
5759 join(' AND ', @search );
5771 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5772 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base