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);
1545 $self->lpr_data(\%opt),
1546 'agentnum' => $self->cust_main->agentnum,
1551 =item fax_invoice HASHREF | [ TEMPLATE ]
1555 Options can be passed as a hashref (recommended) or as a single optional
1558 I<template>, if specified, is the name of a suffix for alternate invoices.
1560 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1566 return if $self->hide;
1567 my $conf = $self->conf;
1569 my( $template, $notice_name );
1572 $template = $opt->{'template'} || '';
1573 $notice_name = $opt->{'notice_name'} || 'Invoice';
1575 $template = scalar(@_) ? shift : '';
1576 $notice_name = 'Invoice';
1579 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1580 unless $conf->exists('invoice_latex');
1582 my $dialstring = $self->cust_main->getfield('fax');
1586 'template' => $template,
1587 'notice_name' => $notice_name,
1590 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1591 'dialstring' => $dialstring,
1593 die $error if $error;
1597 =item batch_invoice [ HASHREF ]
1599 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1600 isn't an open batch, one will be created.
1605 my ($self, $opt) = @_;
1606 my $bill_batch = $self->get_open_bill_batch;
1607 my $cust_bill_batch = FS::cust_bill_batch->new({
1608 batchnum => $bill_batch->batchnum,
1609 invnum => $self->invnum,
1611 return $cust_bill_batch->insert($opt);
1614 =item get_open_batch
1616 Returns the currently open batch as an FS::bill_batch object, creating a new
1617 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1622 sub get_open_bill_batch {
1624 my $conf = $self->conf;
1625 my $hashref = { status => 'O' };
1626 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1627 ? $self->cust_main->agentnum
1629 my $batch = qsearchs('bill_batch', $hashref);
1630 return $batch if $batch;
1631 $batch = FS::bill_batch->new($hashref);
1632 my $error = $batch->insert;
1633 die $error if $error;
1637 =item ftp_invoice [ TEMPLATENAME ]
1639 Sends this invoice data via FTP.
1641 TEMPLATENAME is unused?
1647 my $conf = $self->conf;
1648 my $template = scalar(@_) ? shift : '';
1651 'protocol' => 'ftp',
1652 'server' => $conf->config('cust_bill-ftpserver'),
1653 'username' => $conf->config('cust_bill-ftpusername'),
1654 'password' => $conf->config('cust_bill-ftppassword'),
1655 'dir' => $conf->config('cust_bill-ftpdir'),
1656 'format' => $conf->config('cust_bill-ftpformat'),
1660 =item spool_invoice [ TEMPLATENAME ]
1662 Spools this invoice data (see L<FS::spool_csv>)
1664 TEMPLATENAME is unused?
1670 my $conf = $self->conf;
1671 my $template = scalar(@_) ? shift : '';
1674 'format' => $conf->config('cust_bill-spoolformat'),
1675 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1679 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1681 Like B<send>, but only sends the invoice if it is the newest open invoice for
1686 sub send_if_newest {
1691 grep { $_->owed > 0 }
1692 qsearch('cust_bill', {
1693 'custnum' => $self->custnum,
1694 #'_date' => { op=>'>', value=>$self->_date },
1695 'invnum' => { op=>'>', value=>$self->invnum },
1702 =item send_csv OPTION => VALUE, ...
1704 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1708 protocol - currently only "ftp"
1714 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1715 and YYMMDDHHMMSS is a timestamp.
1717 See L</print_csv> for a description of the output format.
1722 my($self, %opt) = @_;
1726 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1727 mkdir $spooldir, 0700 unless -d $spooldir;
1729 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1730 my $file = "$spooldir/$tracctnum.csv";
1732 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1734 open(CSV, ">$file") or die "can't open $file: $!";
1742 if ( $opt{protocol} eq 'ftp' ) {
1743 eval "use Net::FTP;";
1745 $net = Net::FTP->new($opt{server}) or die @$;
1747 die "unknown protocol: $opt{protocol}";
1750 $net->login( $opt{username}, $opt{password} )
1751 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1753 $net->binary or die "can't set binary mode";
1755 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1757 $net->put($file) or die "can't put $file: $!";
1767 Spools CSV invoice data.
1773 =item format - 'default' or 'billco'
1775 =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>).
1777 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1779 =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.
1786 my($self, %opt) = @_;
1788 my $cust_main = $self->cust_main;
1790 if ( $opt{'dest'} ) {
1791 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1792 $cust_main->invoicing_list;
1793 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1794 || ! keys %invoicing_list;
1797 if ( $opt{'balanceover'} ) {
1799 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1802 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1803 mkdir $spooldir, 0700 unless -d $spooldir;
1805 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1809 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1810 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1813 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1815 open(CSV, ">>$file") or die "can't open $file: $!";
1816 flock(CSV, LOCK_EX);
1821 if ( lc($opt{'format'}) eq 'billco' ) {
1823 flock(CSV, LOCK_UN);
1828 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1831 open(CSV,">>$file") or die "can't open $file: $!";
1832 flock(CSV, LOCK_EX);
1838 flock(CSV, LOCK_UN);
1845 =item print_csv OPTION => VALUE, ...
1847 Returns CSV data for this invoice.
1851 format - 'default' or 'billco'
1853 Returns a list consisting of two scalars. The first is a single line of CSV
1854 header information for this invoice. The second is one or more lines of CSV
1855 detail information for this invoice.
1857 If I<format> is not specified or "default", the fields of the CSV file are as
1860 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1864 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1866 B<record_type> is C<cust_bill> for the initial header line only. The
1867 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1868 fields are filled in.
1870 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1871 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1874 =item invnum - invoice number
1876 =item custnum - customer number
1878 =item _date - invoice date
1880 =item charged - total invoice amount
1882 =item first - customer first name
1884 =item last - customer first name
1886 =item company - company name
1888 =item address1 - address line 1
1890 =item address2 - address line 1
1900 =item pkg - line item description
1902 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1904 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1906 =item sdate - start date for recurring fee
1908 =item edate - end date for recurring fee
1912 If I<format> is "billco", the fields of the header CSV file are as follows:
1914 +-------------------------------------------------------------------+
1915 | FORMAT HEADER FILE |
1916 |-------------------------------------------------------------------|
1917 | Field | Description | Name | Type | Width |
1918 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1919 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1920 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1921 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1922 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1923 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1924 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1925 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1926 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1927 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1928 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1929 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1930 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1931 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1932 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1933 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1934 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1935 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1936 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1937 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1938 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1939 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1940 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1941 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1942 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1943 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1944 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1945 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1946 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1947 +-------+-------------------------------+------------+------+-------+
1949 If I<format> is "billco", the fields of the detail CSV file are as follows:
1951 FORMAT FOR DETAIL FILE
1953 Field | Description | Name | Type | Width
1954 1 | N/A-Leave Empty | RC | CHAR | 2
1955 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1956 3 | Account Number | TRACCTNUM | CHAR | 15
1957 4 | Invoice Number | TRINVOICE | CHAR | 15
1958 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1959 6 | Transaction Detail | DETAILS | CHAR | 100
1960 7 | Amount | AMT | NUM* | 9
1961 8 | Line Format Control** | LNCTRL | CHAR | 2
1962 9 | Grouping Code | GROUP | CHAR | 2
1963 10 | User Defined | ACCT CODE | CHAR | 15
1968 my($self, %opt) = @_;
1970 eval "use Text::CSV_XS";
1973 my $cust_main = $self->cust_main;
1975 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1977 if ( lc($opt{'format'}) eq 'billco' ) {
1980 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1982 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1984 my( $previous_balance, @unused ) = $self->previous; #previous balance
1986 my $pmt_cr_applied = 0;
1987 $pmt_cr_applied += $_->{'amount'}
1988 foreach ( $self->_items_payments, $self->_items_credits ) ;
1990 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1993 '', # 1 | N/A-Leave Empty CHAR 2
1994 '', # 2 | N/A-Leave Empty CHAR 15
1995 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1996 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1997 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1998 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1999 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2000 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2001 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2002 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2003 '', # 10 | Ancillary Billing Information CHAR 30
2004 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2005 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2008 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2011 $duedate, # 14 | Bill Due Date CHAR 10
2013 $previous_balance, # 15 | Previous Balance NUM* 9
2014 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2015 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2016 $totaldue, # 18 | Total Amt Due NUM* 9
2017 $totaldue, # 19 | Total Amt Due NUM* 9
2018 '', # 20 | 30 Day Aging NUM* 9
2019 '', # 21 | 60 Day Aging NUM* 9
2020 '', # 22 | 90 Day Aging NUM* 9
2021 'N', # 23 | Y/N CHAR 1
2022 '', # 24 | Remittance automation CHAR 100
2023 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2024 $self->custnum, # 26 | Customer Reference Number CHAR 15
2025 '0', # 27 | Federal Tax*** NUM* 9
2026 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2027 '0', # 29 | Other Taxes & Fees*** NUM* 9
2030 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2032 my ($previous_balance) = $self->previous;
2033 $previous_balance = sprintf('%.2f', $previous_balance);
2034 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2040 $self->_items_pkg, #_items_nontax? no sections or anything
2045 $cust_main->agentnum,
2046 $cust_main->agent->agent,
2050 $cust_main->company,
2051 $cust_main->address1,
2052 $cust_main->address2,
2058 time2str("%x", $self->_date),
2063 $self->due_date2str("%x"),
2074 time2str("%x", $self->_date),
2075 sprintf("%.2f", $self->charged),
2076 ( map { $cust_main->getfield($_) }
2077 qw( first last company address1 address2 city state zip country ) ),
2079 ) or die "can't create csv";
2082 my $header = $csv->string. "\n";
2085 if ( lc($opt{'format'}) eq 'billco' ) {
2088 foreach my $item ( $self->_items_pkg ) {
2091 '', # 1 | N/A-Leave Empty CHAR 2
2092 '', # 2 | N/A-Leave Empty CHAR 15
2093 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2094 $self->invnum, # 4 | Invoice Number CHAR 15
2095 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2096 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2097 $item->{'amount'}, # 7 | Amount NUM* 9
2098 '', # 8 | Line Format Control** CHAR 2
2099 '', # 9 | Grouping Code CHAR 2
2100 '', # 10 | User Defined CHAR 15
2103 $detail .= $csv->string. "\n";
2107 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2113 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2115 my($pkg, $setup, $recur, $sdate, $edate);
2116 if ( $cust_bill_pkg->pkgnum ) {
2118 ($pkg, $setup, $recur, $sdate, $edate) = (
2119 $cust_bill_pkg->part_pkg->pkg,
2120 ( $cust_bill_pkg->setup != 0
2121 ? sprintf("%.2f", $cust_bill_pkg->setup )
2123 ( $cust_bill_pkg->recur != 0
2124 ? sprintf("%.2f", $cust_bill_pkg->recur )
2126 ( $cust_bill_pkg->sdate
2127 ? time2str("%x", $cust_bill_pkg->sdate)
2129 ($cust_bill_pkg->edate
2130 ?time2str("%x", $cust_bill_pkg->edate)
2134 } else { #pkgnum tax
2135 next unless $cust_bill_pkg->setup != 0;
2136 $pkg = $cust_bill_pkg->desc;
2137 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2138 ( $sdate, $edate ) = ( '', '' );
2144 ( map { '' } (1..11) ),
2145 ($pkg, $setup, $recur, $sdate, $edate)
2146 ) or die "can't create csv";
2148 $detail .= $csv->string. "\n";
2154 ( $header, $detail );
2160 Pays this invoice with a compliemntary payment. If there is an error,
2161 returns the error, otherwise returns false.
2167 my $cust_pay = new FS::cust_pay ( {
2168 'invnum' => $self->invnum,
2169 'paid' => $self->owed,
2172 'payinfo' => $self->cust_main->payinfo,
2180 Attempts to pay this invoice with a credit card payment via a
2181 Business::OnlinePayment realtime gateway. See
2182 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2183 for supported processors.
2189 $self->realtime_bop( 'CC', @_ );
2194 Attempts to pay this invoice with an electronic check (ACH) payment via a
2195 Business::OnlinePayment realtime gateway. See
2196 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2197 for supported processors.
2203 $self->realtime_bop( 'ECHECK', @_ );
2208 Attempts to pay this invoice with phone bill (LEC) payment via a
2209 Business::OnlinePayment realtime gateway. See
2210 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2211 for supported processors.
2217 $self->realtime_bop( 'LEC', @_ );
2221 my( $self, $method ) = (shift,shift);
2222 my $conf = $self->conf;
2225 my $cust_main = $self->cust_main;
2226 my $balance = $cust_main->balance;
2227 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2228 $amount = sprintf("%.2f", $amount);
2229 return "not run (balance $balance)" unless $amount > 0;
2231 my $description = 'Internet Services';
2232 if ( $conf->exists('business-onlinepayment-description') ) {
2233 my $dtempl = $conf->config('business-onlinepayment-description');
2235 my $agent_obj = $cust_main->agent
2236 or die "can't retreive agent for $cust_main (agentnum ".
2237 $cust_main->agentnum. ")";
2238 my $agent = $agent_obj->agent;
2239 my $pkgs = join(', ',
2240 map { $_->part_pkg->pkg }
2241 grep { $_->pkgnum } $self->cust_bill_pkg
2243 $description = eval qq("$dtempl");
2246 $cust_main->realtime_bop($method, $amount,
2247 'description' => $description,
2248 'invnum' => $self->invnum,
2249 #this didn't do what we want, it just calls apply_payments_and_credits
2251 'apply_to_invoice' => 1,
2254 #this changes application behavior: auto payments
2255 #triggered against a specific invoice are now applied
2256 #to that invoice instead of oldest open.
2262 =item batch_card OPTION => VALUE...
2264 Adds a payment for this invoice to the pending credit card batch (see
2265 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2266 runs the payment using a realtime gateway.
2271 my ($self, %options) = @_;
2272 my $cust_main = $self->cust_main;
2274 $options{invnum} = $self->invnum;
2276 $cust_main->batch_card(%options);
2279 sub _agent_template {
2281 $self->cust_main->agent_template;
2284 sub _agent_invoice_from {
2286 $self->cust_main->agent_invoice_from;
2289 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2291 Returns an text invoice, as a list of lines.
2293 Options can be passed as a hashref (recommended) or as a list of time, template
2294 and then any key/value pairs for any other options.
2296 I<time>, if specified, is used to control the printing of overdue messages. The
2297 default is now. It isn't the date of the invoice; that's the `_date' field.
2298 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2299 L<Time::Local> and L<Date::Parse> for conversion functions.
2301 I<template>, if specified, is the name of a suffix for alternate invoices.
2303 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2309 my( $today, $template, %opt );
2311 %opt = %{ shift() };
2312 $today = delete($opt{'time'}) || '';
2313 $template = delete($opt{template}) || '';
2315 ( $today, $template, %opt ) = @_;
2318 my %params = ( 'format' => 'template' );
2319 $params{'time'} = $today if $today;
2320 $params{'template'} = $template if $template;
2321 $params{$_} = $opt{$_}
2322 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2324 $self->print_generic( %params );
2327 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2329 Internal method - returns a filename of a filled-in LaTeX template for this
2330 invoice (Note: add ".tex" to get the actual filename), and a filename of
2331 an associated logo (with the .eps extension included).
2333 See print_ps and print_pdf for methods that return PostScript and PDF output.
2335 Options can be passed as a hashref (recommended) or as a list of time, template
2336 and then any key/value pairs for any other options.
2338 I<time>, if specified, is used to control the printing of overdue messages. The
2339 default is now. It isn't the date of the invoice; that's the `_date' field.
2340 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2341 L<Time::Local> and L<Date::Parse> for conversion functions.
2343 I<template>, if specified, is the name of a suffix for alternate invoices.
2345 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2351 my $conf = $self->conf;
2352 my( $today, $template, %opt );
2354 %opt = %{ shift() };
2355 $today = delete($opt{'time'}) || '';
2356 $template = delete($opt{template}) || '';
2358 ( $today, $template, %opt ) = @_;
2361 my %params = ( 'format' => 'latex' );
2362 $params{'time'} = $today if $today;
2363 $params{'template'} = $template if $template;
2364 $params{$_} = $opt{$_}
2365 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2367 $template ||= $self->_agent_template;
2369 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2370 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2374 ) or die "can't open temp file: $!\n";
2376 my $agentnum = $self->cust_main->agentnum;
2378 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2379 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2380 or die "can't write temp file: $!\n";
2382 print $lh $conf->config_binary('logo.eps', $agentnum)
2383 or die "can't write temp file: $!\n";
2386 $params{'logo_file'} = $lh->filename;
2388 if($conf->exists('invoice-barcode')){
2389 my $png_file = $self->invoice_barcode($dir);
2390 my $eps_file = $png_file;
2391 $eps_file =~ s/\.png$/.eps/g;
2392 $png_file =~ /(barcode.*png)/;
2394 $eps_file =~ /(barcode.*eps)/;
2397 my $curr_dir = cwd();
2399 # after painfuly long experimentation, it was determined that sam2p won't
2400 # accept : and other chars in the path, no matter how hard I tried to
2401 # escape them, hence the chdir (and chdir back, just to be safe)
2402 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2403 or die "sam2p failed: $!\n";
2407 $params{'barcode_file'} = $eps_file;
2410 my @filled_in = $self->print_generic( %params );
2412 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2416 ) or die "can't open temp file: $!\n";
2417 binmode($fh, ':utf8'); # language support
2418 print $fh join('', @filled_in );
2421 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2422 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2426 =item invoice_barcode DIR_OR_FALSE
2428 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2429 it is taken as the temp directory where the PNG file will be generated and the
2430 PNG file name is returned. Otherwise, the PNG image itself is returned.
2434 sub invoice_barcode {
2435 my ($self, $dir) = (shift,shift);
2437 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2438 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2439 my $gd = $gdbar->plot(Height => 30);
2442 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2446 ) or die "can't open temp file: $!\n";
2447 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2448 my $png_file = $bh->filename;
2455 =item print_generic OPTION => VALUE ...
2457 Internal method - returns a filled-in template for this invoice as a scalar.
2459 See print_ps and print_pdf for methods that return PostScript and PDF output.
2461 Non optional options include
2462 format - latex, html, template
2464 Optional options include
2466 template - a value used as a suffix for a configuration template
2468 time - a value used to control the printing of overdue messages. The
2469 default is now. It isn't the date of the invoice; that's the `_date' field.
2470 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2471 L<Time::Local> and L<Date::Parse> for conversion functions.
2475 unsquelch_cdr - overrides any per customer cdr squelching when true
2477 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2479 locale - override customer's locale
2483 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2484 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2485 # yes: fixed width/plain text printing will be borked
2487 my( $self, %params ) = @_;
2488 my $conf = $self->conf;
2489 my $today = $params{today} ? $params{today} : time;
2490 warn "$me print_generic called on $self with suffix $params{template}\n"
2493 my $format = $params{format};
2494 die "Unknown format: $format"
2495 unless $format =~ /^(latex|html|template)$/;
2497 my $cust_main = $self->cust_main;
2498 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2499 unless $cust_main->payname
2500 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2502 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2503 'html' => [ '<%=', '%>' ],
2504 'template' => [ '{', '}' ],
2507 warn "$me print_generic creating template\n"
2510 #create the template
2511 my $template = $params{template} ? $params{template} : $self->_agent_template;
2512 my $templatefile = "invoice_$format";
2513 $templatefile .= "_$template"
2514 if length($template) && $conf->exists($templatefile."_$template");
2515 my @invoice_template = map "$_\n", $conf->config($templatefile)
2516 or die "cannot load config data $templatefile";
2519 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2520 #change this to a die when the old code is removed
2521 warn "old-style invoice template $templatefile; ".
2522 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2523 $old_latex = 'true';
2524 @invoice_template = _translate_old_latex_format(@invoice_template);
2527 warn "$me print_generic creating T:T object\n"
2530 my $text_template = new Text::Template(
2532 SOURCE => \@invoice_template,
2533 DELIMITERS => $delimiters{$format},
2536 warn "$me print_generic compiling T:T object\n"
2539 $text_template->compile()
2540 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2543 # additional substitution could possibly cause breakage in existing templates
2544 my %convert_maps = (
2546 'notes' => sub { map "$_", @_ },
2547 'footer' => sub { map "$_", @_ },
2548 'smallfooter' => sub { map "$_", @_ },
2549 'returnaddress' => sub { map "$_", @_ },
2550 'coupon' => sub { map "$_", @_ },
2551 'summary' => sub { map "$_", @_ },
2557 s/%%(.*)$/<!-- $1 -->/g;
2558 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2559 s/\\begin\{enumerate\}/<ol>/g;
2561 s/\\end\{enumerate\}/<\/ol>/g;
2562 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2571 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2573 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2578 s/\\\\\*?\s*$/<BR>/;
2579 s/\\hyphenation\{[\w\s\-]+}//;
2584 'coupon' => sub { "" },
2585 'summary' => sub { "" },
2592 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2593 s/\\begin\{enumerate\}//g;
2595 s/\\end\{enumerate\}//g;
2596 s/\\textbf\{(.*)\}/$1/g;
2603 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2605 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2610 s/\\\\\*?\s*$/\n/; # dubious
2611 s/\\hyphenation\{[\w\s\-]+}//;
2615 'coupon' => sub { "" },
2616 'summary' => sub { "" },
2621 # hashes for differing output formats
2622 my %nbsps = ( 'latex' => '~',
2623 'html' => '', # '&nbps;' would be nice
2624 'template' => '', # not used
2626 my $nbsp = $nbsps{$format};
2628 my %escape_functions = ( 'latex' => \&_latex_escape,
2629 'html' => \&_html_escape_nbsp,#\&encode_entities,
2630 'template' => sub { shift },
2632 my $escape_function = $escape_functions{$format};
2633 my $escape_function_nonbsp = ($format eq 'html')
2634 ? \&_html_escape : $escape_function;
2636 my %date_formats = ( 'latex' => $date_format_long,
2637 'html' => $date_format_long,
2640 $date_formats{'html'} =~ s/ / /g;
2642 my $date_format = $date_formats{$format};
2644 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2646 'html' => sub { return '<b>'. shift(). '</b>'
2648 'template' => sub { shift },
2650 my $embolden_function = $embolden_functions{$format};
2652 my %newline_tokens = ( 'latex' => '\\\\',
2656 my $newline_token = $newline_tokens{$format};
2658 warn "$me generating template variables\n"
2661 # generate template variables
2664 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2668 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2674 $returnaddress = join("\n",
2675 $conf->config_orbase("invoice_${format}returnaddress", $template)
2678 } elsif ( grep /\S/,
2679 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2681 my $convert_map = $convert_maps{$format}{'returnaddress'};
2684 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2689 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2691 my $convert_map = $convert_maps{$format}{'returnaddress'};
2692 $returnaddress = join( "\n", &$convert_map(
2693 map { s/( {2,})/'~' x length($1)/eg;
2697 ( $conf->config('company_name', $self->cust_main->agentnum),
2698 $conf->config('company_address', $self->cust_main->agentnum),
2705 my $warning = "Couldn't find a return address; ".
2706 "do you need to set the company_address configuration value?";
2708 $returnaddress = $nbsp;
2709 #$returnaddress = $warning;
2713 warn "$me generating invoice data\n"
2716 my $agentnum = $self->cust_main->agentnum;
2718 my %invoice_data = (
2721 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2722 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2723 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2724 'returnaddress' => $returnaddress,
2725 'agent' => &$escape_function($cust_main->agent->agent),
2728 'invnum' => $self->invnum,
2729 'date' => time2str($date_format, $self->_date),
2730 'today' => time2str($date_format_long, $today),
2731 'terms' => $self->terms,
2732 'template' => $template, #params{'template'},
2733 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2734 'current_charges' => sprintf("%.2f", $self->charged),
2735 'duedate' => $self->due_date2str($rdate_format), #date_format?
2738 'custnum' => $cust_main->display_custnum,
2739 'agent_custid' => &$escape_function($cust_main->agent_custid),
2740 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2741 payname company address1 address2 city state zip fax
2745 'ship_enable' => $conf->exists('invoice-ship_address'),
2746 'unitprices' => $conf->exists('invoice-unitprice'),
2747 'smallernotes' => $conf->exists('invoice-smallernotes'),
2748 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2749 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2751 #layout info -- would be fancy to calc some of this and bury the template
2753 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2754 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2755 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2756 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2757 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2758 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2759 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2760 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2761 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2762 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2764 # better hang on to conf_dir for a while (for old templates)
2765 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2767 #these are only used when doing paged plaintext
2774 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2775 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2776 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2777 # eval to avoid death for unimplemented languages
2778 my $dh = eval { Date::Language->new($info{'name'}) } ||
2779 Date::Language->new(); # fall back to English
2780 # prototype here to silence warnings
2781 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2782 # eventually use this date handle everywhere in here, too
2784 my $min_sdate = 999999999999;
2786 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2787 next unless $cust_bill_pkg->pkgnum > 0;
2788 $min_sdate = $cust_bill_pkg->sdate
2789 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2790 $max_edate = $cust_bill_pkg->edate
2791 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2794 $invoice_data{'bill_period'} = '';
2795 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2796 . " to " . time2str('%e %h', $max_edate)
2797 if ($max_edate != 0 && $min_sdate != 999999999999);
2799 $invoice_data{finance_section} = '';
2800 if ( $conf->config('finance_pkgclass') ) {
2802 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2803 $invoice_data{finance_section} = $pkg_class->categoryname;
2805 $invoice_data{finance_amount} = '0.00';
2806 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2808 my $countrydefault = $conf->config('countrydefault') || 'US';
2809 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2810 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2811 my $method = $prefix.$_;
2812 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2814 $invoice_data{'ship_country'} = ''
2815 if ( $invoice_data{'ship_country'} eq $countrydefault );
2817 $invoice_data{'cid'} = $params{'cid'}
2820 if ( $cust_main->country eq $countrydefault ) {
2821 $invoice_data{'country'} = '';
2823 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2827 $invoice_data{'address'} = \@address;
2829 $cust_main->payname.
2830 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2831 ? " (P.O. #". $cust_main->payinfo. ")"
2835 push @address, $cust_main->company
2836 if $cust_main->company;
2837 push @address, $cust_main->address1;
2838 push @address, $cust_main->address2
2839 if $cust_main->address2;
2841 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2842 push @address, $invoice_data{'country'}
2843 if $invoice_data{'country'};
2845 while (scalar(@address) < 5);
2847 $invoice_data{'logo_file'} = $params{'logo_file'}
2848 if $params{'logo_file'};
2849 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2850 if $params{'barcode_file'};
2851 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2852 if $params{'barcode_img'};
2853 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2854 if $params{'barcode_cid'};
2856 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2857 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2858 #my $balance_due = $self->owed + $pr_total - $cr_total;
2859 my $balance_due = $self->owed + $pr_total;
2861 # the customer's current balance as shown on the invoice before this one
2862 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2864 # the change in balance from that invoice to this one
2865 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2867 # the sum of amount owed on all previous invoices
2868 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2870 # the sum of amount owed on all invoices
2871 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2873 # info from customer's last invoice before this one, for some
2875 $invoice_data{'last_bill'} = {};
2876 my $last_bill = $pr_cust_bill[-1];
2878 $invoice_data{'last_bill'} = {
2879 '_date' => $last_bill->_date, #unformatted
2880 # all we need for now
2884 my $summarypage = '';
2885 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2888 $invoice_data{'summarypage'} = $summarypage;
2890 warn "$me substituting variables in notes, footer, smallfooter\n"
2893 my @include = (qw( notes footer smallfooter ));
2894 push @include, 'coupon' unless $params{'no_coupon'};
2895 foreach my $include (@include) {
2897 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2900 if ( $conf->exists($inc_file, $agentnum)
2901 && length( $conf->config($inc_file, $agentnum) ) ) {
2903 @inc_src = $conf->config($inc_file, $agentnum);
2907 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2909 my $convert_map = $convert_maps{$format}{$include};
2911 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2912 s/--\@\]/$delimiters{$format}[1]/g;
2915 &$convert_map( $conf->config($inc_file, $agentnum) );
2919 my $inc_tt = new Text::Template (
2921 SOURCE => [ map "$_\n", @inc_src ],
2922 DELIMITERS => $delimiters{$format},
2923 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2925 unless ( $inc_tt->compile() ) {
2926 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2927 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2931 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2933 $invoice_data{$include} =~ s/\n+$//
2934 if ($format eq 'latex');
2937 # let invoices use either of these as needed
2938 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2939 ? $cust_main->payinfo : '';
2940 $invoice_data{'po_line'} =
2941 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2942 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2945 my %money_chars = ( 'latex' => '',
2946 'html' => $conf->config('money_char') || '$',
2949 my $money_char = $money_chars{$format};
2951 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2952 'html' => $conf->config('money_char') || '$',
2955 my $other_money_char = $other_money_chars{$format};
2956 $invoice_data{'dollar'} = $other_money_char;
2958 my @detail_items = ();
2959 my @total_items = ();
2963 $invoice_data{'detail_items'} = \@detail_items;
2964 $invoice_data{'total_items'} = \@total_items;
2965 $invoice_data{'buf'} = \@buf;
2966 $invoice_data{'sections'} = \@sections;
2968 warn "$me generating sections\n"
2971 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2972 'subtotal' => $other_money_char.
2973 sprintf('%.2f', $pr_total),
2974 'summarized' => '', #why? $summarypage ? 'Y' : '',
2976 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2977 join(' / ', map { $cust_main->balance_date_range(@$_) }
2978 $self->_prior_month30s
2980 if $conf->exists('invoice_include_aging');
2983 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2984 'subtotal' => $taxtotal, # adjusted below
2986 my $tax_weight = _pkg_category($tax_section->{description})
2987 ? _pkg_category($tax_section->{description})->weight
2989 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2990 $tax_section->{'sort_weight'} = $tax_weight;
2993 my $adjusttotal = 0;
2994 my $adjust_section = {
2995 'description' => $self->mt('Credits, Payments, and Adjustments'),
2996 'adjust_section' => 1,
2997 'subtotal' => 0, # adjusted below
2999 my $adjust_weight = _pkg_category($adjust_section->{description})
3000 ? _pkg_category($adjust_section->{description})->weight
3002 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3003 $adjust_section->{'sort_weight'} = $adjust_weight;
3005 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3006 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3007 $invoice_data{'multisection'} = $multisection;
3008 my $late_sections = [];
3009 my $extra_sections = [];
3010 my $extra_lines = ();
3012 my $default_section = { 'description' => '',
3017 if ( $multisection ) {
3018 ($extra_sections, $extra_lines) =
3019 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3020 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3022 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3024 push @detail_items, @$extra_lines if $extra_lines;
3026 $self->_items_sections( $late_sections, # this could stand a refactor
3028 $escape_function_nonbsp,
3032 if ($conf->exists('svc_phone_sections')) {
3033 my ($phone_sections, $phone_lines) =
3034 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3035 push @{$late_sections}, @$phone_sections;
3036 push @detail_items, @$phone_lines;
3038 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3039 my ($accountcode_section, $accountcode_lines) =
3040 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3041 if ( scalar(@$accountcode_lines) ) {
3042 push @{$late_sections}, $accountcode_section;
3043 push @detail_items, @$accountcode_lines;
3046 } else {# not multisection
3047 # make a default section
3048 push @sections, $default_section;
3049 # and calculate the finance charge total, since it won't get done otherwise.
3050 # XXX possibly other totals?
3051 # XXX possibly finance_pkgclass should not be used in this manner?
3052 if ( $conf->exists('finance_pkgclass') ) {
3053 my @finance_charges;
3054 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3055 if ( grep { $_->section eq $invoice_data{finance_section} }
3056 $cust_bill_pkg->cust_bill_pkg_display ) {
3057 # I think these are always setup fees, but just to be sure...
3058 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3061 $invoice_data{finance_amount} =
3062 sprintf('%.2f', sum( @finance_charges ) || 0);
3066 # previous invoice balances in the Previous Charges section if there
3067 # is one, otherwise in the main detail section
3068 if ( $self->can('_items_previous') &&
3069 $self->enable_previous &&
3070 ! $conf->exists('previous_balance-summary_only') ) {
3072 warn "$me adding previous balances\n"
3075 foreach my $line_item ( $self->_items_previous ) {
3078 ext_description => [],
3080 $detail->{'ref'} = $line_item->{'pkgnum'};
3081 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3082 $detail->{'quantity'} = 1;
3083 $detail->{'section'} = $multisection ? $previous_section
3085 $detail->{'description'} = &$escape_function($line_item->{'description'});
3086 if ( exists $line_item->{'ext_description'} ) {
3087 @{$detail->{'ext_description'}} = map {
3088 &$escape_function($_);
3089 } @{$line_item->{'ext_description'}};
3091 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3092 $line_item->{'amount'};
3093 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3095 push @detail_items, $detail;
3096 push @buf, [ $detail->{'description'},
3097 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3103 if ( @pr_cust_bill && $self->enable_previous ) {
3104 push @buf, ['','-----------'];
3105 push @buf, [ $self->mt('Total Previous Balance'),
3106 $money_char. sprintf("%10.2f", $pr_total) ];
3110 if ( $conf->exists('svc_phone-did-summary') ) {
3111 warn "$me adding DID summary\n"
3114 my ($didsummary,$minutes) = $self->_did_summary;
3115 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3117 { 'description' => $didsummary_desc,
3118 'ext_description' => [ $didsummary, $minutes ],
3122 foreach my $section (@sections, @$late_sections) {
3124 warn "$me adding section \n". Dumper($section)
3127 # begin some normalization
3128 $section->{'subtotal'} = $section->{'amount'}
3130 && !exists($section->{subtotal})
3131 && exists($section->{amount});
3133 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3134 if ( $invoice_data{finance_section} &&
3135 $section->{'description'} eq $invoice_data{finance_section} );
3137 $section->{'subtotal'} = $other_money_char.
3138 sprintf('%.2f', $section->{'subtotal'})
3141 # continue some normalization
3142 $section->{'amount'} = $section->{'subtotal'}
3146 if ( $section->{'description'} ) {
3147 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3152 warn "$me setting options\n"
3155 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3157 $options{'section'} = $section if $multisection;
3158 $options{'format'} = $format;
3159 $options{'escape_function'} = $escape_function;
3160 $options{'no_usage'} = 1 unless $unsquelched;
3161 $options{'unsquelched'} = $unsquelched;
3162 $options{'summary_page'} = $summarypage;
3163 $options{'skip_usage'} =
3164 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3165 $options{'multilocation'} = $multilocation;
3166 $options{'multisection'} = $multisection;
3168 warn "$me searching for line items\n"
3171 foreach my $line_item ( $self->_items_pkg(%options) ) {
3173 warn "$me adding line item $line_item\n"
3177 ext_description => [],
3179 $detail->{'ref'} = $line_item->{'pkgnum'};
3180 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3181 $detail->{'quantity'} = $line_item->{'quantity'};
3182 $detail->{'section'} = $section;
3183 $detail->{'description'} = &$escape_function($line_item->{'description'});
3184 if ( exists $line_item->{'ext_description'} ) {
3185 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3187 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3188 $line_item->{'amount'};
3189 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3190 $line_item->{'unit_amount'};
3191 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3193 $detail->{'sdate'} = $line_item->{'sdate'};
3194 $detail->{'edate'} = $line_item->{'edate'};
3195 $detail->{'seconds'} = $line_item->{'seconds'};
3196 $detail->{'svc_label'} = $line_item->{'svc_label'};
3198 push @detail_items, $detail;
3199 push @buf, ( [ $detail->{'description'},
3200 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3202 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3206 if ( $section->{'description'} ) {
3207 push @buf, ( ['','-----------'],
3208 [ $section->{'description'}. ' sub-total',
3209 $section->{'subtotal'} # already formatted this
3218 $invoice_data{current_less_finance} =
3219 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3221 # create a major section for previous balance if we have major sections,
3222 # or if previous_section is in summary form
3223 if ( ( $multisection && $self->enable_previous )
3224 || $conf->exists('previous_balance-summary_only') )
3226 unshift @sections, $previous_section if $pr_total;
3229 warn "$me adding taxes\n"
3232 foreach my $tax ( $self->_items_tax ) {
3234 $taxtotal += $tax->{'amount'};
3236 my $description = &$escape_function( $tax->{'description'} );
3237 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3239 if ( $multisection ) {
3241 my $money = $old_latex ? '' : $money_char;
3242 push @detail_items, {
3243 ext_description => [],
3246 description => $description,
3247 amount => $money. $amount,
3249 section => $tax_section,
3254 push @total_items, {
3255 'total_item' => $description,
3256 'total_amount' => $other_money_char. $amount,
3261 push @buf,[ $description,
3262 $money_char. $amount,
3269 $total->{'total_item'} = $self->mt('Sub-total');
3270 $total->{'total_amount'} =
3271 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3273 if ( $multisection ) {
3274 $tax_section->{'subtotal'} = $other_money_char.
3275 sprintf('%.2f', $taxtotal);
3276 $tax_section->{'pretotal'} = 'New charges sub-total '.
3277 $total->{'total_amount'};
3278 push @sections, $tax_section if $taxtotal;
3280 unshift @total_items, $total;
3283 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3285 push @buf,['','-----------'];
3286 push @buf,[$self->mt(
3287 (!$self->enable_previous)
3289 : 'Total New Charges'
3291 $money_char. sprintf("%10.2f",$self->charged) ];
3294 # calculate total, possibly including total owed on previous
3299 $item = $conf->config('previous_balance-exclude_from_total')
3300 || 'Total New Charges'
3301 if $conf->exists('previous_balance-exclude_from_total');
3302 my $amount = $self->charged;
3303 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3304 $amount += $pr_total;
3307 $total->{'total_item'} = &$embolden_function($self->mt($item));
3308 $total->{'total_amount'} =
3309 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3310 if ( $multisection ) {
3311 if ( $adjust_section->{'sort_weight'} ) {
3312 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3313 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3315 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3316 $other_money_char. sprintf('%.2f', $self->charged );
3319 push @total_items, $total;
3321 push @buf,['','-----------'];
3324 sprintf( '%10.2f', $amount )
3329 # if we're showing previous invoices, also show previous
3330 # credits and payments
3331 if ( $self->enable_previous
3332 and $self->can('_items_credits')
3333 and $self->can('_items_payments') )
3335 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3338 my $credittotal = 0;
3339 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3342 $total->{'total_item'} = &$escape_function($credit->{'description'});
3343 $credittotal += $credit->{'amount'};
3344 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3345 $adjusttotal += $credit->{'amount'};
3346 if ( $multisection ) {
3347 my $money = $old_latex ? '' : $money_char;
3348 push @detail_items, {
3349 ext_description => [],
3352 description => &$escape_function($credit->{'description'}),
3353 amount => $money. $credit->{'amount'},
3355 section => $adjust_section,
3358 push @total_items, $total;
3362 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3365 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3366 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3370 my $paymenttotal = 0;
3371 foreach my $payment ( $self->_items_payments ) {
3373 $total->{'total_item'} = &$escape_function($payment->{'description'});
3374 $paymenttotal += $payment->{'amount'};
3375 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3376 $adjusttotal += $payment->{'amount'};
3377 if ( $multisection ) {
3378 my $money = $old_latex ? '' : $money_char;
3379 push @detail_items, {
3380 ext_description => [],
3383 description => &$escape_function($payment->{'description'}),
3384 amount => $money. $payment->{'amount'},
3386 section => $adjust_section,
3389 push @total_items, $total;
3391 push @buf, [ $payment->{'description'},
3392 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3395 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3397 if ( $multisection ) {
3398 $adjust_section->{'subtotal'} = $other_money_char.
3399 sprintf('%.2f', $adjusttotal);
3400 push @sections, $adjust_section
3401 unless $adjust_section->{sort_weight};
3404 # create Balance Due message
3407 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3408 $total->{'total_amount'} =
3409 &$embolden_function(
3410 $other_money_char. sprintf('%.2f', #why? $summarypage
3411 # ? $self->charged +
3412 # $self->billing_balance
3414 $self->owed + $pr_total
3417 if ( $multisection && !$adjust_section->{sort_weight} ) {
3418 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3419 $total->{'total_amount'};
3421 push @total_items, $total;
3423 push @buf,['','-----------'];
3424 push @buf,[$self->balance_due_msg, $money_char.
3425 sprintf("%10.2f", $balance_due ) ];
3428 if ( $conf->exists('previous_balance-show_credit')
3429 and $cust_main->balance < 0 ) {
3430 my $credit_total = {
3431 'total_item' => &$embolden_function($self->credit_balance_msg),
3432 'total_amount' => &$embolden_function(
3433 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3436 if ( $multisection ) {
3437 $adjust_section->{'posttotal'} .= $newline_token .
3438 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3441 push @total_items, $credit_total;
3443 push @buf,['','-----------'];
3444 push @buf,[$self->credit_balance_msg, $money_char.
3445 sprintf("%10.2f", -$cust_main->balance ) ];
3449 if ( $multisection ) {
3450 if ($conf->exists('svc_phone_sections')) {
3452 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3453 $total->{'total_amount'} =
3454 &$embolden_function(
3455 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3457 my $last_section = pop @sections;
3458 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3459 $total->{'total_amount'};
3460 push @sections, $last_section;
3462 push @sections, @$late_sections
3466 # make a discounts-available section, even without multisection
3467 if ( $conf->exists('discount-show_available')
3468 and my @discounts_avail = $self->_items_discounts_avail ) {
3469 my $discount_section = {
3470 'description' => $self->mt('Discounts Available'),
3475 push @sections, $discount_section;
3476 push @detail_items, map { +{
3477 'ref' => '', #should this be something else?
3478 'section' => $discount_section,
3479 'description' => &$escape_function( $_->{description} ),
3480 'amount' => $money_char . &$escape_function( $_->{amount} ),
3481 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3482 } } @discounts_avail;
3485 # All sections and items are built; now fill in templates.
3486 my @includelist = ();
3487 push @includelist, 'summary' if $summarypage;
3488 foreach my $include ( @includelist ) {
3490 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3493 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3495 @inc_src = $conf->config($inc_file, $agentnum);
3499 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3501 my $convert_map = $convert_maps{$format}{$include};
3503 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3504 s/--\@\]/$delimiters{$format}[1]/g;
3507 &$convert_map( $conf->config($inc_file, $agentnum) );
3511 my $inc_tt = new Text::Template (
3513 SOURCE => [ map "$_\n", @inc_src ],
3514 DELIMITERS => $delimiters{$format},
3515 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3517 unless ( $inc_tt->compile() ) {
3518 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3519 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3523 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3525 $invoice_data{$include} =~ s/\n+$//
3526 if ($format eq 'latex');
3531 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3532 /invoice_lines\((\d*)\)/;
3533 $invoice_lines += $1 || scalar(@buf);
3536 die "no invoice_lines() functions in template?"
3537 if ( $format eq 'template' && !$wasfunc );
3539 if ($format eq 'template') {
3541 if ( $invoice_lines ) {
3542 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3543 $invoice_data{'total_pages'}++
3544 if scalar(@buf) % $invoice_lines;
3547 #setup subroutine for the template
3548 $invoice_data{invoice_lines} = sub {
3549 my $lines = shift || scalar(@buf);
3561 push @collect, split("\n",
3562 $text_template->fill_in( HASH => \%invoice_data )
3564 $invoice_data{'page'}++;
3566 map "$_\n", @collect;
3568 # this is where we actually create the invoice
3569 warn "filling in template for invoice ". $self->invnum. "\n"
3571 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3574 $text_template->fill_in(HASH => \%invoice_data);
3578 # helper routine for generating date ranges
3579 sub _prior_month30s {
3582 [ 1, 2592000 ], # 0-30 days ago
3583 [ 2592000, 5184000 ], # 30-60 days ago
3584 [ 5184000, 7776000 ], # 60-90 days ago
3585 [ 7776000, 0 ], # 90+ days ago
3588 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3589 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3594 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3596 Returns an postscript invoice, as a scalar.
3598 Options can be passed as a hashref (recommended) or as a list of time, template
3599 and then any key/value pairs for any other options.
3601 I<time> an optional value used to control the printing of overdue messages. The
3602 default is now. It isn't the date of the invoice; that's the `_date' field.
3603 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3604 L<Time::Local> and L<Date::Parse> for conversion functions.
3606 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3613 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3614 my $ps = generate_ps($file);
3616 unlink($barcodefile) if $barcodefile;
3621 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3623 Returns an PDF invoice, as a scalar.
3625 Options can be passed as a hashref (recommended) or as a list of time, template
3626 and then any key/value pairs for any other options.
3628 I<time> an optional value used to control the printing of overdue messages. The
3629 default is now. It isn't the date of the invoice; that's the `_date' field.
3630 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3631 L<Time::Local> and L<Date::Parse> for conversion functions.
3633 I<template>, if specified, is the name of a suffix for alternate invoices.
3635 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3642 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3643 my $pdf = generate_pdf($file);
3645 unlink($barcodefile) if $barcodefile;
3650 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3652 Returns an HTML invoice, as a scalar.
3654 I<time> an optional value used to control the printing of overdue messages. The
3655 default is now. It isn't the date of the invoice; that's the `_date' field.
3656 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3657 L<Time::Local> and L<Date::Parse> for conversion functions.
3659 I<template>, if specified, is the name of a suffix for alternate invoices.
3661 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3663 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3664 when emailing the invoice as part of a multipart/related MIME email.
3672 %params = %{ shift() };
3674 $params{'time'} = shift;
3675 $params{'template'} = shift;
3676 $params{'cid'} = shift;
3679 $params{'format'} = 'html';
3681 $self->print_generic( %params );
3684 # quick subroutine for print_latex
3686 # There are ten characters that LaTeX treats as special characters, which
3687 # means that they do not simply typeset themselves:
3688 # # $ % & ~ _ ^ \ { }
3690 # TeX ignores blanks following an escaped character; if you want a blank (as
3691 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3695 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3696 $value =~ s/([<>])/\$$1\$/g;
3702 encode_entities($value);
3706 sub _html_escape_nbsp {
3707 my $value = _html_escape(shift);
3708 $value =~ s/ +/ /g;
3712 #utility methods for print_*
3714 sub _translate_old_latex_format {
3715 warn "_translate_old_latex_format called\n"
3722 if ( $line =~ /^%%Detail\s*$/ ) {
3724 push @template, q![@--!,
3725 q! foreach my $_tr_line (@detail_items) {!,
3726 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3727 q! $_tr_line->{'description'} .= !,
3728 q! "\\tabularnewline\n~~".!,
3729 q! join( "\\tabularnewline\n~~",!,
3730 q! @{$_tr_line->{'ext_description'}}!,
3734 while ( ( my $line_item_line = shift )
3735 !~ /^%%EndDetail\s*$/ ) {
3736 $line_item_line =~ s/'/\\'/g; # nice LTS
3737 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3738 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3739 push @template, " \$OUT .= '$line_item_line';";
3742 push @template, '}',
3745 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3747 push @template, '[@--',
3748 ' foreach my $_tr_line (@total_items) {';
3750 while ( ( my $total_item_line = shift )
3751 !~ /^%%EndTotalDetails\s*$/ ) {
3752 $total_item_line =~ s/'/\\'/g; # nice LTS
3753 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3754 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3755 push @template, " \$OUT .= '$total_item_line';";
3758 push @template, '}',
3762 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3763 push @template, $line;
3769 warn "$_\n" foreach @template;
3777 my $conf = $self->conf;
3779 #check for an invoice-specific override
3780 return $self->invoice_terms if $self->invoice_terms;
3782 #check for a customer- specific override
3783 my $cust_main = $self->cust_main;
3784 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3786 #use configured default
3787 $conf->config('invoice_default_terms') || '';
3793 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3794 $duedate = $self->_date() + ( $1 * 86400 );
3801 $self->due_date ? time2str(shift, $self->due_date) : '';
3804 sub balance_due_msg {
3806 my $msg = $self->mt('Balance Due');
3807 return $msg unless $self->terms;
3808 if ( $self->due_date ) {
3809 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3810 $self->due_date2str($date_format);
3811 } elsif ( $self->terms ) {
3812 $msg .= ' - '. $self->terms;
3817 sub balance_due_date {
3819 my $conf = $self->conf;
3821 if ( $conf->exists('invoice_default_terms')
3822 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3823 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3828 sub credit_balance_msg {
3830 $self->mt('Credit Balance Remaining')
3833 =item invnum_date_pretty
3835 Returns a string with the invoice number and date, for example:
3836 "Invoice #54 (3/20/2008)"
3840 sub invnum_date_pretty {
3842 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3847 Returns a string with the date, for example: "3/20/2008"
3853 time2str($date_format, $self->_date);
3856 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3858 Generate section information for all items appearing on this invoice.
3859 This will only be called for multi-section invoices.
3861 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3862 related display records (L<FS::cust_bill_pkg_display>) and organize
3863 them into two groups ("early" and "late" according to whether they come
3864 before or after the total), then into sections. A subtotal is calculated
3867 Section descriptions are returned in sort weight order. Each consists
3868 of a hash containing:
3870 description: the package category name, escaped
3871 subtotal: the total charges in that section
3872 tax_section: a flag indicating that the section contains only tax charges
3873 summarized: same as tax_section, for some reason
3874 sort_weight: the package category's sort weight
3876 If 'condense' is set on the display record, it also contains everything
3877 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3878 coderefs to generate parts of the invoice. This is not advised.
3882 LATE: an arrayref to push the "late" section hashes onto. The "early"
3883 group is simply returned from the method.
3885 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3886 Turning this on has the following effects:
3887 - Ignores display items with the 'summary' flag.
3888 - Combines all items into the "early" group.
3889 - Creates sections for all non-disabled package categories, even if they
3890 have no charges on this invoice, as well as a section with no name.
3892 ESCAPE: an escape function to use for section titles.
3894 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3895 sorted list. If there are any of these, section subtotals exclude
3898 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3899 passed through to C<_condense_section()>.
3903 use vars qw(%pkg_category_cache);
3904 sub _items_sections {
3907 my $summarypage = shift;
3909 my $extra_sections = shift;
3913 my %late_subtotal = ();
3916 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3919 my $usage = $cust_bill_pkg->usage;
3921 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3922 next if ( $display->summary && $summarypage );
3924 my $section = $display->section;
3925 my $type = $display->type;
3927 $not_tax{$section} = 1
3928 unless $cust_bill_pkg->pkgnum == 0;
3930 if ( $display->post_total && !$summarypage ) {
3931 if (! $type || $type eq 'S') {
3932 $late_subtotal{$section} += $cust_bill_pkg->setup
3933 if $cust_bill_pkg->setup != 0
3934 || $cust_bill_pkg->setup_show_zero;
3938 $late_subtotal{$section} += $cust_bill_pkg->recur
3939 if $cust_bill_pkg->recur != 0
3940 || $cust_bill_pkg->recur_show_zero;
3943 if ($type && $type eq 'R') {
3944 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3945 if $cust_bill_pkg->recur != 0
3946 || $cust_bill_pkg->recur_show_zero;
3949 if ($type && $type eq 'U') {
3950 $late_subtotal{$section} += $usage
3951 unless scalar(@$extra_sections);
3956 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3958 if (! $type || $type eq 'S') {
3959 $subtotal{$section} += $cust_bill_pkg->setup
3960 if $cust_bill_pkg->setup != 0
3961 || $cust_bill_pkg->setup_show_zero;
3965 $subtotal{$section} += $cust_bill_pkg->recur
3966 if $cust_bill_pkg->recur != 0
3967 || $cust_bill_pkg->recur_show_zero;
3970 if ($type && $type eq 'R') {
3971 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3972 if $cust_bill_pkg->recur != 0
3973 || $cust_bill_pkg->recur_show_zero;
3976 if ($type && $type eq 'U') {
3977 $subtotal{$section} += $usage
3978 unless scalar(@$extra_sections);
3987 %pkg_category_cache = ();
3989 push @$late, map { { 'description' => &{$escape}($_),
3990 'subtotal' => $late_subtotal{$_},
3992 'sort_weight' => ( _pkg_category($_)
3993 ? _pkg_category($_)->weight
3996 ((_pkg_category($_) && _pkg_category($_)->condense)
3997 ? $self->_condense_section($format)
4001 sort _sectionsort keys %late_subtotal;
4004 if ( $summarypage ) {
4005 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4006 map { $_->categoryname } qsearch('pkg_category', {});
4007 push @sections, '' if exists($subtotal{''});
4009 @sections = keys %subtotal;
4012 my @early = map { { 'description' => &{$escape}($_),
4013 'subtotal' => $subtotal{$_},
4014 'summarized' => $not_tax{$_} ? '' : 'Y',
4015 'tax_section' => $not_tax{$_} ? '' : 'Y',
4016 'sort_weight' => ( _pkg_category($_)
4017 ? _pkg_category($_)->weight
4020 ((_pkg_category($_) && _pkg_category($_)->condense)
4021 ? $self->_condense_section($format)
4026 push @early, @$extra_sections if $extra_sections;
4028 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4032 #helper subs for above
4035 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4039 my $categoryname = shift;
4040 $pkg_category_cache{$categoryname} ||=
4041 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4044 my %condensed_format = (
4045 'label' => [ qw( Description Qty Amount ) ],
4047 sub { shift->{description} },
4048 sub { shift->{quantity} },
4049 sub { my($href, %opt) = @_;
4050 ($opt{dollar} || ''). $href->{amount};
4053 'align' => [ qw( l r r ) ],
4054 'span' => [ qw( 5 1 1 ) ], # unitprices?
4055 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4058 sub _condense_section {
4059 my ( $self, $format ) = ( shift, shift );
4061 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4062 qw( description_generator
4065 total_line_generator
4070 sub _condensed_generator_defaults {
4071 my ( $self, $format ) = ( shift, shift );
4072 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4081 sub _condensed_header_generator {
4082 my ( $self, $format ) = ( shift, shift );
4084 my ( $f, $prefix, $suffix, $separator, $column ) =
4085 _condensed_generator_defaults($format);
4087 if ($format eq 'latex') {
4088 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4089 $suffix = "\\\\\n\\hline";
4092 sub { my ($d,$a,$s,$w) = @_;
4093 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4095 } elsif ( $format eq 'html' ) {
4096 $prefix = '<th></th>';
4100 sub { my ($d,$a,$s,$w) = @_;
4101 return qq!<th align="$html_align{$a}">$d</th>!;
4109 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4111 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4114 $prefix. join($separator, @result). $suffix;
4119 sub _condensed_description_generator {
4120 my ( $self, $format ) = ( shift, shift );
4122 my ( $f, $prefix, $suffix, $separator, $column ) =
4123 _condensed_generator_defaults($format);
4125 my $money_char = '$';
4126 if ($format eq 'latex') {
4127 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4129 $separator = " & \n";
4131 sub { my ($d,$a,$s,$w) = @_;
4132 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4134 $money_char = '\\dollar';
4135 }elsif ( $format eq 'html' ) {
4136 $prefix = '"><td align="center"></td>';
4140 sub { my ($d,$a,$s,$w) = @_;
4141 return qq!<td align="$html_align{$a}">$d</td>!;
4143 #$money_char = $conf->config('money_char') || '$';
4144 $money_char = ''; # this is madness
4152 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4154 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4156 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4157 map { $f->{$_}->[$i] } qw(align span width)
4161 $prefix. join( $separator, @result ). $suffix;
4166 sub _condensed_total_generator {
4167 my ( $self, $format ) = ( shift, shift );
4169 my ( $f, $prefix, $suffix, $separator, $column ) =
4170 _condensed_generator_defaults($format);
4173 if ($format eq 'latex') {
4176 $separator = " & \n";
4178 sub { my ($d,$a,$s,$w) = @_;
4179 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4181 }elsif ( $format eq 'html' ) {
4185 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4187 sub { my ($d,$a,$s,$w) = @_;
4188 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4197 # my $r = &{$f->{fields}->[$i]}(@args);
4198 # $r .= ' Total' unless $i;
4200 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4202 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4203 map { $f->{$_}->[$i] } qw(align span width)
4207 $prefix. join( $separator, @result ). $suffix;
4212 =item total_line_generator FORMAT
4214 Returns a coderef used for generation of invoice total line items for this
4215 usage_class. FORMAT is either html or latex
4219 # should not be used: will have issues with hash element names (description vs
4220 # total_item and amount vs total_amount -- another array of functions?
4222 sub _condensed_total_line_generator {
4223 my ( $self, $format ) = ( shift, shift );
4225 my ( $f, $prefix, $suffix, $separator, $column ) =
4226 _condensed_generator_defaults($format);
4229 if ($format eq 'latex') {
4232 $separator = " & \n";
4234 sub { my ($d,$a,$s,$w) = @_;
4235 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4237 }elsif ( $format eq 'html' ) {
4241 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4243 sub { my ($d,$a,$s,$w) = @_;
4244 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4253 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4255 &{$column}( &{$f->{fields}->[$i]}(@args),
4256 map { $f->{$_}->[$i] } qw(align span width)
4260 $prefix. join( $separator, @result ). $suffix;
4265 #sub _items_extra_usage_sections {
4267 # my $escape = shift;
4269 # my %sections = ();
4271 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4272 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4274 # next unless $cust_bill_pkg->pkgnum > 0;
4276 # foreach my $section ( keys %usage_class ) {
4278 # my $usage = $cust_bill_pkg->usage($section);
4280 # next unless $usage && $usage > 0;
4282 # $sections{$section} ||= 0;
4283 # $sections{$section} += $usage;
4289 # map { { 'description' => &{$escape}($_),
4290 # 'subtotal' => $sections{$_},
4291 # 'summarized' => '',
4292 # 'tax_section' => '',
4295 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4299 sub _items_extra_usage_sections {
4301 my $conf = $self->conf;
4309 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4311 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4312 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4313 next unless $cust_bill_pkg->pkgnum > 0;
4315 foreach my $classnum ( keys %usage_class ) {
4316 my $section = $usage_class{$classnum}->classname;
4317 $classnums{$section} = $classnum;
4319 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4320 my $amount = $detail->amount;
4321 next unless $amount && $amount > 0;
4323 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4324 $sections{$section}{amount} += $amount; #subtotal
4325 $sections{$section}{calls}++;
4326 $sections{$section}{duration} += $detail->duration;
4328 my $desc = $detail->regionname;
4329 my $description = $desc;
4330 $description = substr($desc, 0, $maxlength). '...'
4331 if $format eq 'latex' && length($desc) > $maxlength;
4333 $lines{$section}{$desc} ||= {
4334 description => &{$escape}($description),
4335 #pkgpart => $part_pkg->pkgpart,
4336 pkgnum => $cust_bill_pkg->pkgnum,
4341 #unit_amount => $cust_bill_pkg->unitrecur,
4342 quantity => $cust_bill_pkg->quantity,
4343 product_code => 'N/A',
4344 ext_description => [],
4347 $lines{$section}{$desc}{amount} += $amount;
4348 $lines{$section}{$desc}{calls}++;
4349 $lines{$section}{$desc}{duration} += $detail->duration;
4355 my %sectionmap = ();
4356 foreach (keys %sections) {
4357 my $usage_class = $usage_class{$classnums{$_}};
4358 $sectionmap{$_} = { 'description' => &{$escape}($_),
4359 'amount' => $sections{$_}{amount}, #subtotal
4360 'calls' => $sections{$_}{calls},
4361 'duration' => $sections{$_}{duration},
4363 'tax_section' => '',
4364 'sort_weight' => $usage_class->weight,
4365 ( $usage_class->format
4366 ? ( map { $_ => $usage_class->$_($format) }
4367 qw( description_generator header_generator total_generator total_line_generator )
4374 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4378 foreach my $section ( keys %lines ) {
4379 foreach my $line ( keys %{$lines{$section}} ) {
4380 my $l = $lines{$section}{$line};
4381 $l->{section} = $sectionmap{$section};
4382 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4383 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4388 return(\@sections, \@lines);
4394 my $end = $self->_date;
4396 # start at date of previous invoice + 1 second or 0 if no previous invoice
4397 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4398 $start = 0 if !$start;
4401 my $cust_main = $self->cust_main;
4402 my @pkgs = $cust_main->all_pkgs;
4403 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4406 foreach my $pkg ( @pkgs ) {
4407 my @h_cust_svc = $pkg->h_cust_svc($end);
4408 foreach my $h_cust_svc ( @h_cust_svc ) {
4409 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4410 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4412 my $inserted = $h_cust_svc->date_inserted;
4413 my $deleted = $h_cust_svc->date_deleted;
4414 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4416 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4418 # DID either activated or ported in; cannot be both for same DID simultaneously
4419 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4420 && (!$phone_inserted->lnp_status
4421 || $phone_inserted->lnp_status eq ''
4422 || $phone_inserted->lnp_status eq 'native')) {
4425 else { # this one not so clean, should probably move to (h_)svc_phone
4426 my $phone_portedin = qsearchs( 'h_svc_phone',
4427 { 'svcnum' => $h_cust_svc->svcnum,
4428 'lnp_status' => 'portedin' },
4429 FS::h_svc_phone->sql_h_searchs($end),
4431 $num_portedin++ if $phone_portedin;
4434 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4435 if($deleted >= $start && $deleted <= $end && $phone_deleted
4436 && (!$phone_deleted->lnp_status
4437 || $phone_deleted->lnp_status ne 'portingout')) {
4440 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4441 && $phone_deleted->lnp_status
4442 && $phone_deleted->lnp_status eq 'portingout') {
4446 # increment usage minutes
4447 if ( $phone_inserted ) {
4448 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4449 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4452 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4455 # don't look at this service again
4456 push @seen, $h_cust_svc->svcnum;
4460 $minutes = sprintf("%d", $minutes);
4461 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4462 . "$num_deactivated Ported-Out: $num_portedout ",
4463 "Total Minutes: $minutes");
4466 sub _items_accountcode_cdr {
4471 my $section = { 'amount' => 0,
4474 'sort_weight' => '',
4476 'description' => 'Usage by Account Code',
4482 my %accountcodes = ();
4484 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4485 next unless $cust_bill_pkg->pkgnum > 0;
4487 my @header = $cust_bill_pkg->details_header;
4488 next unless scalar(@header);
4489 $section->{'header'} = join(',',@header);
4491 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4493 $section->{'header'} = $detail->formatted('format' => $format)
4494 if($detail->detail eq $section->{'header'});
4496 my $accountcode = $detail->accountcode;
4497 next unless $accountcode;
4499 my $amount = $detail->amount;
4500 next unless $amount && $amount > 0;
4502 $accountcodes{$accountcode} ||= {
4503 description => $accountcode,
4510 product_code => 'N/A',
4511 section => $section,
4512 ext_description => [ $section->{'header'} ],
4516 $section->{'amount'} += $amount;
4517 $accountcodes{$accountcode}{'amount'} += $amount;
4518 $accountcodes{$accountcode}{calls}++;
4519 $accountcodes{$accountcode}{duration} += $detail->duration;
4520 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4524 foreach my $l ( values %accountcodes ) {
4525 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4526 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4527 foreach my $sorted_detail ( @sorted_detail ) {
4528 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4530 delete $l->{detail_temp};
4534 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4536 return ($section,\@sorted_lines);
4539 sub _items_svc_phone_sections {
4541 my $conf = $self->conf;
4549 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4551 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4552 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4554 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4555 next unless $cust_bill_pkg->pkgnum > 0;
4557 my @header = $cust_bill_pkg->details_header;
4558 next unless scalar(@header);
4560 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4562 my $phonenum = $detail->phonenum;
4563 next unless $phonenum;
4565 my $amount = $detail->amount;
4566 next unless $amount && $amount > 0;
4568 $sections{$phonenum} ||= { 'amount' => 0,
4571 'sort_weight' => -1,
4572 'phonenum' => $phonenum,
4574 $sections{$phonenum}{amount} += $amount; #subtotal
4575 $sections{$phonenum}{calls}++;
4576 $sections{$phonenum}{duration} += $detail->duration;
4578 my $desc = $detail->regionname;
4579 my $description = $desc;
4580 $description = substr($desc, 0, $maxlength). '...'
4581 if $format eq 'latex' && length($desc) > $maxlength;
4583 $lines{$phonenum}{$desc} ||= {
4584 description => &{$escape}($description),
4585 #pkgpart => $part_pkg->pkgpart,
4593 product_code => 'N/A',
4594 ext_description => [],
4597 $lines{$phonenum}{$desc}{amount} += $amount;
4598 $lines{$phonenum}{$desc}{calls}++;
4599 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4601 my $line = $usage_class{$detail->classnum}->classname;
4602 $sections{"$phonenum $line"} ||=
4606 'sort_weight' => $usage_class{$detail->classnum}->weight,
4607 'phonenum' => $phonenum,
4608 'header' => [ @header ],
4610 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4611 $sections{"$phonenum $line"}{calls}++;
4612 $sections{"$phonenum $line"}{duration} += $detail->duration;
4614 $lines{"$phonenum $line"}{$desc} ||= {
4615 description => &{$escape}($description),
4616 #pkgpart => $part_pkg->pkgpart,
4624 product_code => 'N/A',
4625 ext_description => [],
4628 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4629 $lines{"$phonenum $line"}{$desc}{calls}++;
4630 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4631 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4632 $detail->formatted('format' => $format);
4637 my %sectionmap = ();
4638 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4639 foreach ( keys %sections ) {
4640 my @header = @{ $sections{$_}{header} || [] };
4642 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4643 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4644 my $usage_class = $summary ? $simple : $usage_simple;
4645 my $ending = $summary ? ' usage charges' : '';
4648 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4650 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4651 'amount' => $sections{$_}{amount}, #subtotal
4652 'calls' => $sections{$_}{calls},
4653 'duration' => $sections{$_}{duration},
4655 'tax_section' => '',
4656 'phonenum' => $sections{$_}{phonenum},
4657 'sort_weight' => $sections{$_}{sort_weight},
4658 'post_total' => $summary, #inspire pagebreak
4660 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4661 qw( description_generator
4664 total_line_generator
4671 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4672 $a->{sort_weight} <=> $b->{sort_weight}
4677 foreach my $section ( keys %lines ) {
4678 foreach my $line ( keys %{$lines{$section}} ) {
4679 my $l = $lines{$section}{$line};
4680 $l->{section} = $sectionmap{$section};
4681 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4682 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4687 if($conf->exists('phone_usage_class_summary')) {
4688 # this only works with Latex
4692 # after this, we'll have only two sections per DID:
4693 # Calls Summary and Calls Detail
4694 foreach my $section ( @sections ) {
4695 if($section->{'post_total'}) {
4696 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4697 $section->{'total_line_generator'} = sub { '' };
4698 $section->{'total_generator'} = sub { '' };
4699 $section->{'header_generator'} = sub { '' };
4700 $section->{'description_generator'} = '';
4701 push @newsections, $section;
4702 my %calls_detail = %$section;
4703 $calls_detail{'post_total'} = '';
4704 $calls_detail{'sort_weight'} = '';
4705 $calls_detail{'description_generator'} = sub { '' };
4706 $calls_detail{'header_generator'} = sub {
4707 return ' & Date/Time & Called Number & Duration & Price'
4708 if $format eq 'latex';
4711 $calls_detail{'description'} = 'Calls Detail: '
4712 . $section->{'phonenum'};
4713 push @newsections, \%calls_detail;
4717 # after this, each usage class is collapsed/summarized into a single
4718 # line under the Calls Summary section
4719 foreach my $newsection ( @newsections ) {
4720 if($newsection->{'post_total'}) { # this means Calls Summary
4721 foreach my $section ( @sections ) {
4722 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4723 && !$section->{'post_total'});
4724 my $newdesc = $section->{'description'};
4725 my $tn = $section->{'phonenum'};
4726 $newdesc =~ s/$tn//g;
4727 my $line = { ext_description => [],
4731 calls => $section->{'calls'},
4732 section => $newsection,
4733 duration => $section->{'duration'},
4734 description => $newdesc,
4735 amount => sprintf("%.2f",$section->{'amount'}),
4736 product_code => 'N/A',
4738 push @newlines, $line;
4743 # after this, Calls Details is populated with all CDRs
4744 foreach my $newsection ( @newsections ) {
4745 if(!$newsection->{'post_total'}) { # this means Calls Details
4746 foreach my $line ( @lines ) {
4747 next unless (scalar(@{$line->{'ext_description'}}) &&
4748 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4750 my @extdesc = @{$line->{'ext_description'}};
4752 foreach my $extdesc ( @extdesc ) {
4753 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4754 push @newextdesc, $extdesc;
4756 $line->{'ext_description'} = \@newextdesc;
4757 $line->{'section'} = $newsection;
4758 push @newlines, $line;
4763 return(\@newsections, \@newlines);
4766 return(\@sections, \@lines);
4770 sub _items { # seems to be unused
4773 #my @display = scalar(@_)
4775 # : qw( _items_previous _items_pkg );
4776 # #: qw( _items_pkg );
4777 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4778 my @display = qw( _items_previous _items_pkg );
4781 foreach my $display ( @display ) {
4782 push @b, $self->$display(@_);
4787 sub _items_previous {
4789 my $conf = $self->conf;
4790 my $cust_main = $self->cust_main;
4791 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4793 foreach ( @pr_cust_bill ) {
4794 my $date = $conf->exists('invoice_show_prior_due_date')
4795 ? 'due '. $_->due_date2str($date_format)
4796 : time2str($date_format, $_->_date);
4798 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4799 #'pkgpart' => 'N/A',
4801 'amount' => sprintf("%.2f", $_->owed),
4807 # 'description' => 'Previous Balance',
4808 # #'pkgpart' => 'N/A',
4809 # 'pkgnum' => 'N/A',
4810 # 'amount' => sprintf("%10.2f", $pr_total ),
4811 # 'ext_description' => [ map {
4812 # "Invoice ". $_->invnum.
4813 # " (". time2str("%x",$_->_date). ") ".
4814 # sprintf("%10.2f", $_->owed)
4815 # } @pr_cust_bill ],
4820 =item _items_pkg [ OPTIONS ]
4822 Return line item hashes for each package item on this invoice. Nearly
4825 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4827 The only OPTIONS accepted is 'section', which may point to a hashref
4828 with a key named 'condensed', which may have a true value. If it
4829 does, this method tries to merge identical items into items with
4830 'quantity' equal to the number of items (not the sum of their
4831 separate quantities, for some reason).
4839 warn "$me _items_pkg searching for all package line items\n"
4842 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4844 warn "$me _items_pkg filtering line items\n"
4846 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4848 if ($options{section} && $options{section}->{condensed}) {
4850 warn "$me _items_pkg condensing section\n"
4854 local $Storable::canonical = 1;
4855 foreach ( @items ) {
4857 delete $item->{ref};
4858 delete $item->{ext_description};
4859 my $key = freeze($item);
4860 $itemshash{$key} ||= 0;
4861 $itemshash{$key} ++; # += $item->{quantity};
4863 @items = sort { $a->{description} cmp $b->{description} }
4864 map { my $i = thaw($_);
4865 $i->{quantity} = $itemshash{$_};
4867 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4873 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4880 return 0 unless $a->itemdesc cmp $b->itemdesc;
4881 return -1 if $b->itemdesc eq 'Tax';
4882 return 1 if $a->itemdesc eq 'Tax';
4883 return -1 if $b->itemdesc eq 'Other surcharges';
4884 return 1 if $a->itemdesc eq 'Other surcharges';
4885 $a->itemdesc cmp $b->itemdesc;
4890 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4891 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4894 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4896 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4897 list of hashrefs describing the line items they generate on the invoice.
4899 OPTIONS may include:
4901 format: the invoice format.
4903 escape_function: the function used to escape strings.
4905 DEPRECATED? (expensive, mostly unused?)
4906 format_function: the function used to format CDRs.
4908 section: a hashref containing 'description'; if this is present,
4909 cust_bill_pkg_display records not belonging to this section are
4912 multisection: a flag indicating that this is a multisection invoice,
4913 which does something complicated.
4915 multilocation: a flag to display the location label for the package.
4917 Returns a list of hashrefs, each of which may contain:
4919 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4920 ext_description, which is an arrayref of detail lines to show below
4925 sub _items_cust_bill_pkg {
4927 my $conf = $self->conf;
4928 my $cust_bill_pkgs = shift;
4931 my $format = $opt{format} || '';
4932 my $escape_function = $opt{escape_function} || sub { shift };
4933 my $format_function = $opt{format_function} || '';
4934 my $no_usage = $opt{no_usage} || '';
4935 my $unsquelched = $opt{unsquelched} || ''; #unused
4936 my $section = $opt{section}->{description} if $opt{section};
4937 my $summary_page = $opt{summary_page} || ''; #unused
4938 my $multilocation = $opt{multilocation} || '';
4939 my $multisection = $opt{multisection} || '';
4940 my $discount_show_always = 0;
4942 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4944 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4947 my ($s, $r, $u) = ( undef, undef, undef );
4948 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4951 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4952 if ( $_ && !$cust_bill_pkg->hidden ) {
4953 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4954 $_->{amount} =~ s/^\-0\.00$/0.00/;
4955 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4957 if $_->{amount} != 0
4958 || $discount_show_always
4959 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4960 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4966 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4968 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4969 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4972 foreach my $display ( grep { defined($section)
4973 ? $_->section eq $section
4976 #grep { !$_->summary || !$summary_page } # bunk!
4977 grep { !$_->summary || $multisection }
4978 @cust_bill_pkg_display
4982 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4983 $display->billpkgdisplaynum. "\n"
4986 my $type = $display->type;
4988 my $desc = $cust_bill_pkg->desc;
4989 $desc = substr($desc, 0, $maxlength). '...'
4990 if $format eq 'latex' && length($desc) > $maxlength;
4992 my %details_opt = ( 'format' => $format,
4993 'escape_function' => $escape_function,
4994 'format_function' => $format_function,
4995 'no_usage' => $opt{'no_usage'},
4998 if ( $cust_bill_pkg->pkgnum > 0 ) {
5000 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5003 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5005 # which pkgpart to show for display purposes?
5006 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5008 # start/end dates for invoice formats that do nonstandard
5010 my %item_dates = ();
5011 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5012 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5014 if ( (!$type || $type eq 'S')
5015 && ( $cust_bill_pkg->setup != 0
5016 || $cust_bill_pkg->setup_show_zero
5021 warn "$me _items_cust_bill_pkg adding setup\n"
5024 my $description = $desc;
5025 $description .= ' Setup'
5026 if $cust_bill_pkg->recur != 0
5027 || $discount_show_always
5028 || $cust_bill_pkg->recur_show_zero;
5032 unless ( $cust_pkg->part_pkg->hide_svc_detail
5033 || $cust_bill_pkg->hidden )
5036 my @svc_labels = map &{$escape_function}($_),
5037 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5038 push @d, @svc_labels
5039 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5040 $svc_label = $svc_labels[0];
5042 if ( $multilocation ) {
5043 my $loc = $cust_pkg->location_label;
5044 $loc = substr($loc, 0, $maxlength). '...'
5045 if $format eq 'latex' && length($loc) > $maxlength;
5046 push @d, &{$escape_function}($loc);
5049 } #unless hiding service details
5051 push @d, $cust_bill_pkg->details(%details_opt)
5052 if $cust_bill_pkg->recur == 0;
5054 if ( $cust_bill_pkg->hidden ) {
5055 $s->{amount} += $cust_bill_pkg->setup;
5056 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5057 push @{ $s->{ext_description} }, @d;
5061 description => $description,
5062 pkgpart => $pkgpart,
5063 pkgnum => $cust_bill_pkg->pkgnum,
5064 amount => $cust_bill_pkg->setup,
5065 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5066 unit_amount => $cust_bill_pkg->unitsetup,
5067 quantity => $cust_bill_pkg->quantity,
5068 ext_description => \@d,
5069 svc_label => ($svc_label || ''),
5075 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5077 $cust_bill_pkg->recur != 0
5078 || $cust_bill_pkg->setup == 0
5079 || $discount_show_always
5080 || $cust_bill_pkg->recur_show_zero
5085 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5088 my $is_summary = $display->summary;
5089 my $description = ($is_summary && $type && $type eq 'U')
5090 ? "Usage charges" : $desc;
5092 my $part_pkg = $cust_pkg->part_pkg;
5094 #pry be a bit more efficient to look some of this conf stuff up
5097 $conf->exists('disable_line_item_date_ranges')
5098 || $part_pkg->option('disable_line_item_date_ranges',1)
5099 || ! $cust_bill_pkg->sdate
5100 || ! $cust_bill_pkg->edate
5103 my $date_style = '';
5104 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5105 $cust_main->agentnum
5107 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5108 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5109 $cust_main->agentnum
5111 if ( defined($date_style) && $date_style eq 'month_of' ) {
5112 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5113 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5114 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5115 $cust_main->agentnum
5117 $desc .= ' ' unless $desc =~ /\s$/;
5118 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5120 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5121 " - ". time2str($date_format, $cust_bill_pkg->edate);
5123 $description .= " ($time_period)";
5127 my @seconds = (); # for display of usage info
5130 #at least until cust_bill_pkg has "past" ranges in addition to
5131 #the "future" sdate/edate ones... see #3032
5132 my @dates = ( $self->_date );
5133 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5134 push @dates, $prev->sdate if $prev;
5135 push @dates, undef if !$prev;
5137 unless ( $cust_pkg->part_pkg->hide_svc_detail
5138 || $cust_bill_pkg->itemdesc
5139 || $cust_bill_pkg->hidden
5140 || $is_summary && $type && $type eq 'U' )
5143 warn "$me _items_cust_bill_pkg adding service details\n"
5146 my @svc_labels = map &{$escape_function}($_),
5147 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5148 push @d, @svc_labels
5149 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5150 $svc_label = $svc_labels[0];
5152 warn "$me _items_cust_bill_pkg done adding service details\n"
5155 if ( $multilocation ) {
5156 my $loc = $cust_pkg->location_label;
5157 $loc = substr($loc, 0, $maxlength). '...'
5158 if $format eq 'latex' && length($loc) > $maxlength;
5159 push @d, &{$escape_function}($loc);
5162 # Display of seconds_since_sqlradacct:
5163 # On the invoice, when processing @detail_items, look for a field
5164 # named 'seconds'. This will contain total seconds for each
5165 # service, in the same order as @ext_description. For services
5166 # that don't support this it will show undef.
5167 if ( $conf->exists('svc_acct-usage_seconds')
5168 and ! $cust_bill_pkg->pkgpart_override ) {
5169 foreach my $cust_svc (
5170 $cust_pkg->h_cust_svc(@dates, 'I')
5173 # eval because not having any part_export_usage exports
5174 # is a fatal error, last_bill/_date because that's how
5175 # sqlradius_hour billing does it
5177 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5179 push @seconds, $sec;
5181 } #if svc_acct-usage_seconds
5185 unless ( $is_summary ) {
5186 warn "$me _items_cust_bill_pkg adding details\n"
5189 #instead of omitting details entirely in this case (unwanted side
5190 # effects), just omit CDRs
5191 $details_opt{'no_usage'} = 1
5192 if $type && $type eq 'R';
5194 push @d, $cust_bill_pkg->details(%details_opt);
5197 warn "$me _items_cust_bill_pkg calculating amount\n"
5202 $amount = $cust_bill_pkg->recur;
5203 } elsif ($type eq 'R') {
5204 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5205 } elsif ($type eq 'U') {
5206 $amount = $cust_bill_pkg->usage;
5209 if ( !$type || $type eq 'R' ) {
5211 warn "$me _items_cust_bill_pkg adding recur\n"
5214 if ( $cust_bill_pkg->hidden ) {
5215 $r->{amount} += $amount;
5216 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5217 push @{ $r->{ext_description} }, @d;
5220 description => $description,
5221 pkgpart => $pkgpart,
5222 pkgnum => $cust_bill_pkg->pkgnum,
5224 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5225 unit_amount => $cust_bill_pkg->unitrecur,
5226 quantity => $cust_bill_pkg->quantity,
5228 ext_description => \@d,
5229 svc_label => ($svc_label || ''),
5231 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5234 } else { # $type eq 'U'
5236 warn "$me _items_cust_bill_pkg adding usage\n"
5239 if ( $cust_bill_pkg->hidden ) {
5240 $u->{amount} += $amount;
5241 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5242 push @{ $u->{ext_description} }, @d;
5245 description => $description,
5246 pkgpart => $pkgpart,
5247 pkgnum => $cust_bill_pkg->pkgnum,
5249 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5250 unit_amount => $cust_bill_pkg->unitrecur,
5251 quantity => $cust_bill_pkg->quantity,
5253 ext_description => \@d,
5258 } # recurring or usage with recurring charge
5260 } else { #pkgnum tax or one-shot line item (??)
5262 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5265 if ( $cust_bill_pkg->setup != 0 ) {
5267 'description' => $desc,
5268 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5271 if ( $cust_bill_pkg->recur != 0 ) {
5273 'description' => "$desc (".
5274 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5275 time2str($date_format, $cust_bill_pkg->edate). ')',
5276 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5284 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5285 && $conf->exists('discount-show-always'));
5289 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5291 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5292 $_->{amount} =~ s/^\-0\.00$/0.00/;
5293 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5295 if $_->{amount} != 0
5296 || $discount_show_always
5297 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5298 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5302 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5309 sub _items_credits {
5310 my( $self, %opt ) = @_;
5311 my $trim_len = $opt{'trim_len'} || 60;
5315 foreach ( $self->cust_credited ) {
5317 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5319 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5320 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5321 $reason = " ($reason) " if $reason;
5324 #'description' => 'Credit ref\#'. $_->crednum.
5325 # " (". time2str("%x",$_->cust_credit->_date) .")".
5327 'description' => $self->mt('Credit applied').' '.
5328 time2str($date_format,$_->cust_credit->_date). $reason,
5329 'amount' => sprintf("%.2f",$_->amount),
5337 sub _items_payments {
5341 #get & print payments
5342 foreach ( $self->cust_bill_pay ) {
5344 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5346 my $desc = $self->mt('Payment received').' '.
5347 time2str($date_format,$_->cust_pay->_date );
5348 $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
5349 if ( $self->conf->exists('invoice_payment_details') );
5352 'description' => $desc,
5353 'amount' => sprintf("%.2f", $_->amount )
5362 =item _items_discounts_avail
5364 Returns an array of line item hashrefs representing available term discounts
5365 for this invoice. This makes the same assumptions that apply to term
5366 discounts in general: that the package is billed monthly, at a flat rate,
5367 with no usage charges. A prorated first month will be handled, as will
5368 a setup fee if the discount is allowed to apply to setup fees.
5372 sub _items_discounts_avail {
5374 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5376 my %plans = $self->discount_plans;
5378 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5382 my $plan = $plans{$months};
5384 my $term_total = sprintf('%.2f', $plan->discounted_total);
5385 my $percent = sprintf('%.0f',
5386 100 * (1 - $term_total / $plan->base_total) );
5387 my $permonth = sprintf('%.2f', $term_total / $months);
5388 my $detail = $self->mt('discount on item'). ' '.
5389 join(', ', map { "#$_" } $plan->pkgnums)
5392 # discounts for non-integer months don't work anyway
5393 $months = sprintf("%d", $months);
5396 description => $self->mt('Save [_1]% by paying for [_2] months',
5398 amount => $self->mt('[_1] ([_2] per month)',
5399 $term_total, $money_char.$permonth),
5400 ext_description => ($detail || ''),
5403 sort { $b <=> $a } keys %plans;
5407 =item call_details [ OPTION => VALUE ... ]
5409 Returns an array of CSV strings representing the call details for this invoice
5410 The only option available is the boolean prepend_billed_number
5415 my ($self, %opt) = @_;
5417 my $format_function = sub { shift };
5419 if ($opt{prepend_billed_number}) {
5420 $format_function = sub {
5424 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5429 my @details = map { $_->details( 'format_function' => $format_function,
5430 'escape_function' => sub{ return() },
5434 $self->cust_bill_pkg;
5435 my $header = $details[0];
5436 ( $header, grep { $_ ne $header } @details );
5446 =item process_reprint
5450 sub process_reprint {
5451 process_re_X('print', @_);
5454 =item process_reemail
5458 sub process_reemail {
5459 process_re_X('email', @_);
5467 process_re_X('fax', @_);
5475 process_re_X('ftp', @_);
5482 sub process_respool {
5483 process_re_X('spool', @_);
5486 use Storable qw(thaw);
5490 my( $method, $job ) = ( shift, shift );
5491 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5493 my $param = thaw(decode_base64(shift));
5494 warn Dumper($param) if $DEBUG;
5505 my($method, $job, %param ) = @_;
5507 warn "re_X $method for job $job with param:\n".
5508 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5511 #some false laziness w/search/cust_bill.html
5513 my $orderby = 'ORDER BY cust_bill._date';
5515 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5517 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5519 my @cust_bill = qsearch( {
5520 #'select' => "cust_bill.*",
5521 'table' => 'cust_bill',
5522 'addl_from' => $addl_from,
5524 'extra_sql' => $extra_sql,
5525 'order_by' => $orderby,
5529 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5531 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5534 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5535 foreach my $cust_bill ( @cust_bill ) {
5536 $cust_bill->$method();
5538 if ( $job ) { #progressbar foo
5540 if ( time - $min_sec > $last ) {
5541 my $error = $job->update_statustext(
5542 int( 100 * $num / scalar(@cust_bill) )
5544 die $error if $error;
5555 =head1 CLASS METHODS
5561 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5566 my ($class, $start, $end) = @_;
5568 $class->paid_sql($start, $end). ' - '.
5569 $class->credited_sql($start, $end);
5574 Returns an SQL fragment to retreive the net amount (charged minus credited).
5579 my ($class, $start, $end) = @_;
5580 'charged - '. $class->credited_sql($start, $end);
5585 Returns an SQL fragment to retreive the amount paid against this invoice.
5590 my ($class, $start, $end) = @_;
5591 $start &&= "AND cust_bill_pay._date <= $start";
5592 $end &&= "AND cust_bill_pay._date > $end";
5593 $start = '' unless defined($start);
5594 $end = '' unless defined($end);
5595 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5596 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5601 Returns an SQL fragment to retreive the amount credited against this invoice.
5606 my ($class, $start, $end) = @_;
5607 $start &&= "AND cust_credit_bill._date <= $start";
5608 $end &&= "AND cust_credit_bill._date > $end";
5609 $start = '' unless defined($start);
5610 $end = '' unless defined($end);
5611 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5612 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5617 Returns an SQL fragment to retrieve the due date of an invoice.
5618 Currently only supported on PostgreSQL.
5623 my $conf = new FS::Conf;
5627 cust_bill.invoice_terms,
5628 cust_main.invoice_terms,
5629 \''.($conf->config('invoice_default_terms') || '').'\'
5630 ), E\'Net (\\\\d+)\'
5632 ) * 86400 + cust_bill._date'
5635 =item search_sql_where HASHREF
5637 Class method which returns an SQL WHERE fragment to search for parameters
5638 specified in HASHREF. Valid parameters are
5644 List reference of start date, end date, as UNIX timestamps.
5654 List reference of charged limits (exclusive).
5658 List reference of charged limits (exclusive).
5662 flag, return open invoices only
5666 flag, return net invoices only
5670 =item newest_percust
5674 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5678 sub search_sql_where {
5679 my($class, $param) = @_;
5681 warn "$me search_sql_where called with params: \n".
5682 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5688 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5689 push @search, "cust_main.agentnum = $1";
5693 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5694 push @search, "cust_main.refnum = $1";
5698 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5699 push @search, "cust_bill.custnum = $1";
5703 if ( $param->{'cust_classnum'} ) {
5704 my $classnums = $param->{'cust_classnum'};
5705 $classnums = [ $classnums ] if !ref($classnums);
5706 $classnums = [ grep /^\d+$/, @$classnums ];
5707 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5712 if ( $param->{_date} ) {
5713 my($beginning, $ending) = @{$param->{_date}};
5715 push @search, "cust_bill._date >= $beginning",
5716 "cust_bill._date < $ending";
5720 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5721 push @search, "cust_bill.invnum >= $1";
5723 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5724 push @search, "cust_bill.invnum <= $1";
5728 if ( $param->{charged} ) {
5729 my @charged = ref($param->{charged})
5730 ? @{ $param->{charged} }
5731 : ($param->{charged});
5733 push @search, map { s/^charged/cust_bill.charged/; $_; }
5737 my $owed_sql = FS::cust_bill->owed_sql;
5740 if ( $param->{owed} ) {
5741 my @owed = ref($param->{owed})
5742 ? @{ $param->{owed} }
5744 push @search, map { s/^owed/$owed_sql/; $_; }
5749 push @search, "0 != $owed_sql"
5750 if $param->{'open'};
5751 push @search, '0 != '. FS::cust_bill->net_sql
5755 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5756 if $param->{'days'};
5759 if ( $param->{'newest_percust'} ) {
5761 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5762 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5764 my @newest_where = map { my $x = $_;
5765 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5768 grep ! /^cust_main./, @search;
5769 my $newest_where = scalar(@newest_where)
5770 ? ' AND '. join(' AND ', @newest_where)
5774 push @search, "cust_bill._date = (
5775 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5776 WHERE newest_cust_bill.custnum = cust_bill.custnum
5782 #promised_date - also has an option to accept nulls
5783 if ( $param->{promised_date} ) {
5784 my($beginning, $ending, $null) = @{$param->{promised_date}};
5786 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5787 "cust_bill.promised_date < $ending )" .
5788 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5791 #agent virtualization
5792 my $curuser = $FS::CurrentUser::CurrentUser;
5793 if ( $curuser->username eq 'fs_queue'
5794 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5796 my $newuser = qsearchs('access_user', {
5797 'username' => $username,
5801 $curuser = $newuser;
5803 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5806 push @search, $curuser->agentnums_sql;
5808 join(' AND ', @search );
5820 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5821 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base