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' => $self->_date,
2730 'date' => time2str($date_format, $self->_date),
2731 'today' => time2str($date_format_long, $today),
2732 'terms' => $self->terms,
2733 'template' => $template, #params{'template'},
2734 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2735 'current_charges' => sprintf("%.2f", $self->charged),
2736 'duedate' => $self->due_date2str($rdate_format), #date_format?
2739 'custnum' => $cust_main->display_custnum,
2740 'agent_custid' => &$escape_function($cust_main->agent_custid),
2741 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2742 payname company address1 address2 city state zip fax
2746 'ship_enable' => $conf->exists('invoice-ship_address'),
2747 'unitprices' => $conf->exists('invoice-unitprice'),
2748 'smallernotes' => $conf->exists('invoice-smallernotes'),
2749 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2750 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2752 #layout info -- would be fancy to calc some of this and bury the template
2754 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2755 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2756 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2757 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2758 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2759 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2760 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2761 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2762 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2763 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2765 # better hang on to conf_dir for a while (for old templates)
2766 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2768 #these are only used when doing paged plaintext
2775 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2776 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2777 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2778 # eval to avoid death for unimplemented languages
2779 my $dh = eval { Date::Language->new($info{'name'}) } ||
2780 Date::Language->new(); # fall back to English
2781 # prototype here to silence warnings
2782 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2783 # eventually use this date handle everywhere in here, too
2785 my $min_sdate = 999999999999;
2787 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2788 next unless $cust_bill_pkg->pkgnum > 0;
2789 $min_sdate = $cust_bill_pkg->sdate
2790 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2791 $max_edate = $cust_bill_pkg->edate
2792 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2795 $invoice_data{'bill_period'} = '';
2796 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2797 . " to " . time2str('%e %h', $max_edate)
2798 if ($max_edate != 0 && $min_sdate != 999999999999);
2800 $invoice_data{finance_section} = '';
2801 if ( $conf->config('finance_pkgclass') ) {
2803 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2804 $invoice_data{finance_section} = $pkg_class->categoryname;
2806 $invoice_data{finance_amount} = '0.00';
2807 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2809 my $countrydefault = $conf->config('countrydefault') || 'US';
2810 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2811 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2812 my $method = $prefix.$_;
2813 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2815 $invoice_data{'ship_country'} = ''
2816 if ( $invoice_data{'ship_country'} eq $countrydefault );
2818 $invoice_data{'cid'} = $params{'cid'}
2821 if ( $cust_main->country eq $countrydefault ) {
2822 $invoice_data{'country'} = '';
2824 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2828 $invoice_data{'address'} = \@address;
2830 $cust_main->payname.
2831 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2832 ? " (P.O. #". $cust_main->payinfo. ")"
2836 push @address, $cust_main->company
2837 if $cust_main->company;
2838 push @address, $cust_main->address1;
2839 push @address, $cust_main->address2
2840 if $cust_main->address2;
2842 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2843 push @address, $invoice_data{'country'}
2844 if $invoice_data{'country'};
2846 while (scalar(@address) < 5);
2848 $invoice_data{'logo_file'} = $params{'logo_file'}
2849 if $params{'logo_file'};
2850 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2851 if $params{'barcode_file'};
2852 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2853 if $params{'barcode_img'};
2854 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2855 if $params{'barcode_cid'};
2857 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2858 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2859 #my $balance_due = $self->owed + $pr_total - $cr_total;
2860 my $balance_due = $self->owed + $pr_total;
2862 # the customer's current balance as shown on the invoice before this one
2863 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2865 # the change in balance from that invoice to this one
2866 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2868 # the sum of amount owed on all previous invoices
2869 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2871 # the sum of amount owed on all invoices
2872 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2874 # info from customer's last invoice before this one, for some
2876 $invoice_data{'last_bill'} = {};
2877 my $last_bill = $pr_cust_bill[-1];
2879 $invoice_data{'last_bill'} = {
2880 '_date' => $last_bill->_date, #unformatted
2881 # all we need for now
2885 my $summarypage = '';
2886 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2889 $invoice_data{'summarypage'} = $summarypage;
2891 warn "$me substituting variables in notes, footer, smallfooter\n"
2894 my @include = (qw( notes footer smallfooter ));
2895 push @include, 'coupon' unless $params{'no_coupon'};
2896 foreach my $include (@include) {
2898 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2901 if ( $conf->exists($inc_file, $agentnum)
2902 && length( $conf->config($inc_file, $agentnum) ) ) {
2904 @inc_src = $conf->config($inc_file, $agentnum);
2908 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2910 my $convert_map = $convert_maps{$format}{$include};
2912 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2913 s/--\@\]/$delimiters{$format}[1]/g;
2916 &$convert_map( $conf->config($inc_file, $agentnum) );
2920 my $inc_tt = new Text::Template (
2922 SOURCE => [ map "$_\n", @inc_src ],
2923 DELIMITERS => $delimiters{$format},
2924 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2926 unless ( $inc_tt->compile() ) {
2927 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2928 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2932 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2934 $invoice_data{$include} =~ s/\n+$//
2935 if ($format eq 'latex');
2938 # let invoices use either of these as needed
2939 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2940 ? $cust_main->payinfo : '';
2941 $invoice_data{'po_line'} =
2942 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2943 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2946 my %money_chars = ( 'latex' => '',
2947 'html' => $conf->config('money_char') || '$',
2950 my $money_char = $money_chars{$format};
2952 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2953 'html' => $conf->config('money_char') || '$',
2956 my $other_money_char = $other_money_chars{$format};
2957 $invoice_data{'dollar'} = $other_money_char;
2959 my @detail_items = ();
2960 my @total_items = ();
2964 $invoice_data{'detail_items'} = \@detail_items;
2965 $invoice_data{'total_items'} = \@total_items;
2966 $invoice_data{'buf'} = \@buf;
2967 $invoice_data{'sections'} = \@sections;
2969 warn "$me generating sections\n"
2972 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2973 'subtotal' => $other_money_char.
2974 sprintf('%.2f', $pr_total),
2975 'summarized' => '', #why? $summarypage ? 'Y' : '',
2977 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2978 join(' / ', map { $cust_main->balance_date_range(@$_) }
2979 $self->_prior_month30s
2981 if $conf->exists('invoice_include_aging');
2984 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2985 'subtotal' => $taxtotal, # adjusted below
2987 my $tax_weight = _pkg_category($tax_section->{description})
2988 ? _pkg_category($tax_section->{description})->weight
2990 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2991 $tax_section->{'sort_weight'} = $tax_weight;
2994 my $adjusttotal = 0;
2995 my $adjust_section = {
2996 'description' => $self->mt('Credits, Payments, and Adjustments'),
2997 'adjust_section' => 1,
2998 'subtotal' => 0, # adjusted below
3000 my $adjust_weight = _pkg_category($adjust_section->{description})
3001 ? _pkg_category($adjust_section->{description})->weight
3003 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3004 $adjust_section->{'sort_weight'} = $adjust_weight;
3006 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3007 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3008 $invoice_data{'multisection'} = $multisection;
3009 my $late_sections = [];
3010 my $extra_sections = [];
3011 my $extra_lines = ();
3013 my $default_section = { 'description' => '',
3018 if ( $multisection ) {
3019 ($extra_sections, $extra_lines) =
3020 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3021 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3023 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3025 push @detail_items, @$extra_lines if $extra_lines;
3027 $self->_items_sections( $late_sections, # this could stand a refactor
3029 $escape_function_nonbsp,
3033 if ($conf->exists('svc_phone_sections')) {
3034 my ($phone_sections, $phone_lines) =
3035 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3036 push @{$late_sections}, @$phone_sections;
3037 push @detail_items, @$phone_lines;
3039 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3040 my ($accountcode_section, $accountcode_lines) =
3041 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3042 if ( scalar(@$accountcode_lines) ) {
3043 push @{$late_sections}, $accountcode_section;
3044 push @detail_items, @$accountcode_lines;
3047 } else {# not multisection
3048 # make a default section
3049 push @sections, $default_section;
3050 # and calculate the finance charge total, since it won't get done otherwise.
3051 # XXX possibly other totals?
3052 # XXX possibly finance_pkgclass should not be used in this manner?
3053 if ( $conf->exists('finance_pkgclass') ) {
3054 my @finance_charges;
3055 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3056 if ( grep { $_->section eq $invoice_data{finance_section} }
3057 $cust_bill_pkg->cust_bill_pkg_display ) {
3058 # I think these are always setup fees, but just to be sure...
3059 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3062 $invoice_data{finance_amount} =
3063 sprintf('%.2f', sum( @finance_charges ) || 0);
3067 # previous invoice balances in the Previous Charges section if there
3068 # is one, otherwise in the main detail section
3069 if ( $self->can('_items_previous') &&
3070 $self->enable_previous &&
3071 ! $conf->exists('previous_balance-summary_only') ) {
3073 warn "$me adding previous balances\n"
3076 foreach my $line_item ( $self->_items_previous ) {
3079 ext_description => [],
3081 $detail->{'ref'} = $line_item->{'pkgnum'};
3082 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3083 $detail->{'quantity'} = 1;
3084 $detail->{'section'} = $multisection ? $previous_section
3086 $detail->{'description'} = &$escape_function($line_item->{'description'});
3087 if ( exists $line_item->{'ext_description'} ) {
3088 @{$detail->{'ext_description'}} = map {
3089 &$escape_function($_);
3090 } @{$line_item->{'ext_description'}};
3092 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3093 $line_item->{'amount'};
3094 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3096 push @detail_items, $detail;
3097 push @buf, [ $detail->{'description'},
3098 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3104 if ( @pr_cust_bill && $self->enable_previous ) {
3105 push @buf, ['','-----------'];
3106 push @buf, [ $self->mt('Total Previous Balance'),
3107 $money_char. sprintf("%10.2f", $pr_total) ];
3111 if ( $conf->exists('svc_phone-did-summary') ) {
3112 warn "$me adding DID summary\n"
3115 my ($didsummary,$minutes) = $self->_did_summary;
3116 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3118 { 'description' => $didsummary_desc,
3119 'ext_description' => [ $didsummary, $minutes ],
3123 foreach my $section (@sections, @$late_sections) {
3125 warn "$me adding section \n". Dumper($section)
3128 # begin some normalization
3129 $section->{'subtotal'} = $section->{'amount'}
3131 && !exists($section->{subtotal})
3132 && exists($section->{amount});
3134 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3135 if ( $invoice_data{finance_section} &&
3136 $section->{'description'} eq $invoice_data{finance_section} );
3138 $section->{'subtotal'} = $other_money_char.
3139 sprintf('%.2f', $section->{'subtotal'})
3142 # continue some normalization
3143 $section->{'amount'} = $section->{'subtotal'}
3147 if ( $section->{'description'} ) {
3148 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3153 warn "$me setting options\n"
3156 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3158 $options{'section'} = $section if $multisection;
3159 $options{'format'} = $format;
3160 $options{'escape_function'} = $escape_function;
3161 $options{'no_usage'} = 1 unless $unsquelched;
3162 $options{'unsquelched'} = $unsquelched;
3163 $options{'summary_page'} = $summarypage;
3164 $options{'skip_usage'} =
3165 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3166 $options{'multilocation'} = $multilocation;
3167 $options{'multisection'} = $multisection;
3169 warn "$me searching for line items\n"
3172 foreach my $line_item ( $self->_items_pkg(%options) ) {
3174 warn "$me adding line item $line_item\n"
3178 ext_description => [],
3180 $detail->{'ref'} = $line_item->{'pkgnum'};
3181 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3182 $detail->{'quantity'} = $line_item->{'quantity'};
3183 $detail->{'section'} = $section;
3184 $detail->{'description'} = &$escape_function($line_item->{'description'});
3185 if ( exists $line_item->{'ext_description'} ) {
3186 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3188 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3189 $line_item->{'amount'};
3190 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3191 $line_item->{'unit_amount'};
3192 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3194 $detail->{'sdate'} = $line_item->{'sdate'};
3195 $detail->{'edate'} = $line_item->{'edate'};
3196 $detail->{'seconds'} = $line_item->{'seconds'};
3197 $detail->{'svc_label'} = $line_item->{'svc_label'};
3199 push @detail_items, $detail;
3200 push @buf, ( [ $detail->{'description'},
3201 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3203 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3207 if ( $section->{'description'} ) {
3208 push @buf, ( ['','-----------'],
3209 [ $section->{'description'}. ' sub-total',
3210 $section->{'subtotal'} # already formatted this
3219 $invoice_data{current_less_finance} =
3220 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3222 # create a major section for previous balance if we have major sections,
3223 # or if previous_section is in summary form
3224 if ( ( $multisection && $self->enable_previous )
3225 || $conf->exists('previous_balance-summary_only') )
3227 unshift @sections, $previous_section if $pr_total;
3230 warn "$me adding taxes\n"
3233 foreach my $tax ( $self->_items_tax ) {
3235 $taxtotal += $tax->{'amount'};
3237 my $description = &$escape_function( $tax->{'description'} );
3238 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3240 if ( $multisection ) {
3242 my $money = $old_latex ? '' : $money_char;
3243 push @detail_items, {
3244 ext_description => [],
3247 description => $description,
3248 amount => $money. $amount,
3250 section => $tax_section,
3255 push @total_items, {
3256 'total_item' => $description,
3257 'total_amount' => $other_money_char. $amount,
3262 push @buf,[ $description,
3263 $money_char. $amount,
3270 $total->{'total_item'} = $self->mt('Sub-total');
3271 $total->{'total_amount'} =
3272 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3274 if ( $multisection ) {
3275 $tax_section->{'subtotal'} = $other_money_char.
3276 sprintf('%.2f', $taxtotal);
3277 $tax_section->{'pretotal'} = 'New charges sub-total '.
3278 $total->{'total_amount'};
3279 push @sections, $tax_section if $taxtotal;
3281 unshift @total_items, $total;
3284 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3286 push @buf,['','-----------'];
3287 push @buf,[$self->mt(
3288 (!$self->enable_previous)
3290 : 'Total New Charges'
3292 $money_char. sprintf("%10.2f",$self->charged) ];
3295 # calculate total, possibly including total owed on previous
3300 $item = $conf->config('previous_balance-exclude_from_total')
3301 || 'Total New Charges'
3302 if $conf->exists('previous_balance-exclude_from_total');
3303 my $amount = $self->charged;
3304 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3305 $amount += $pr_total;
3308 $total->{'total_item'} = &$embolden_function($self->mt($item));
3309 $total->{'total_amount'} =
3310 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3311 if ( $multisection ) {
3312 if ( $adjust_section->{'sort_weight'} ) {
3313 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3314 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3316 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3317 $other_money_char. sprintf('%.2f', $self->charged );
3320 push @total_items, $total;
3322 push @buf,['','-----------'];
3325 sprintf( '%10.2f', $amount )
3330 # if we're showing previous invoices, also show previous
3331 # credits and payments
3332 if ( $self->enable_previous
3333 and $self->can('_items_credits')
3334 and $self->can('_items_payments') )
3336 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3339 my $credittotal = 0;
3340 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3343 $total->{'total_item'} = &$escape_function($credit->{'description'});
3344 $credittotal += $credit->{'amount'};
3345 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3346 $adjusttotal += $credit->{'amount'};
3347 if ( $multisection ) {
3348 my $money = $old_latex ? '' : $money_char;
3349 push @detail_items, {
3350 ext_description => [],
3353 description => &$escape_function($credit->{'description'}),
3354 amount => $money. $credit->{'amount'},
3356 section => $adjust_section,
3359 push @total_items, $total;
3363 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3366 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3367 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3371 my $paymenttotal = 0;
3372 foreach my $payment ( $self->_items_payments ) {
3374 $total->{'total_item'} = &$escape_function($payment->{'description'});
3375 $paymenttotal += $payment->{'amount'};
3376 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3377 $adjusttotal += $payment->{'amount'};
3378 if ( $multisection ) {
3379 my $money = $old_latex ? '' : $money_char;
3380 push @detail_items, {
3381 ext_description => [],
3384 description => &$escape_function($payment->{'description'}),
3385 amount => $money. $payment->{'amount'},
3387 section => $adjust_section,
3390 push @total_items, $total;
3392 push @buf, [ $payment->{'description'},
3393 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3396 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3398 if ( $multisection ) {
3399 $adjust_section->{'subtotal'} = $other_money_char.
3400 sprintf('%.2f', $adjusttotal);
3401 push @sections, $adjust_section
3402 unless $adjust_section->{sort_weight};
3405 # create Balance Due message
3408 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3409 $total->{'total_amount'} =
3410 &$embolden_function(
3411 $other_money_char. sprintf('%.2f', #why? $summarypage
3412 # ? $self->charged +
3413 # $self->billing_balance
3415 $self->owed + $pr_total
3418 if ( $multisection && !$adjust_section->{sort_weight} ) {
3419 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3420 $total->{'total_amount'};
3422 push @total_items, $total;
3424 push @buf,['','-----------'];
3425 push @buf,[$self->balance_due_msg, $money_char.
3426 sprintf("%10.2f", $balance_due ) ];
3429 if ( $conf->exists('previous_balance-show_credit')
3430 and $cust_main->balance < 0 ) {
3431 my $credit_total = {
3432 'total_item' => &$embolden_function($self->credit_balance_msg),
3433 'total_amount' => &$embolden_function(
3434 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3437 if ( $multisection ) {
3438 $adjust_section->{'posttotal'} .= $newline_token .
3439 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3442 push @total_items, $credit_total;
3444 push @buf,['','-----------'];
3445 push @buf,[$self->credit_balance_msg, $money_char.
3446 sprintf("%10.2f", -$cust_main->balance ) ];
3450 if ( $multisection ) {
3451 if ($conf->exists('svc_phone_sections')) {
3453 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3454 $total->{'total_amount'} =
3455 &$embolden_function(
3456 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3458 my $last_section = pop @sections;
3459 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3460 $total->{'total_amount'};
3461 push @sections, $last_section;
3463 push @sections, @$late_sections
3467 # make a discounts-available section, even without multisection
3468 if ( $conf->exists('discount-show_available')
3469 and my @discounts_avail = $self->_items_discounts_avail ) {
3470 my $discount_section = {
3471 'description' => $self->mt('Discounts Available'),
3476 push @sections, $discount_section;
3477 push @detail_items, map { +{
3478 'ref' => '', #should this be something else?
3479 'section' => $discount_section,
3480 'description' => &$escape_function( $_->{description} ),
3481 'amount' => $money_char . &$escape_function( $_->{amount} ),
3482 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3483 } } @discounts_avail;
3486 # All sections and items are built; now fill in templates.
3487 my @includelist = ();
3488 push @includelist, 'summary' if $summarypage;
3489 foreach my $include ( @includelist ) {
3491 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3494 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3496 @inc_src = $conf->config($inc_file, $agentnum);
3500 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3502 my $convert_map = $convert_maps{$format}{$include};
3504 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3505 s/--\@\]/$delimiters{$format}[1]/g;
3508 &$convert_map( $conf->config($inc_file, $agentnum) );
3512 my $inc_tt = new Text::Template (
3514 SOURCE => [ map "$_\n", @inc_src ],
3515 DELIMITERS => $delimiters{$format},
3516 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3518 unless ( $inc_tt->compile() ) {
3519 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3520 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3524 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3526 $invoice_data{$include} =~ s/\n+$//
3527 if ($format eq 'latex');
3532 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3533 /invoice_lines\((\d*)\)/;
3534 $invoice_lines += $1 || scalar(@buf);
3537 die "no invoice_lines() functions in template?"
3538 if ( $format eq 'template' && !$wasfunc );
3540 if ($format eq 'template') {
3542 if ( $invoice_lines ) {
3543 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3544 $invoice_data{'total_pages'}++
3545 if scalar(@buf) % $invoice_lines;
3548 #setup subroutine for the template
3549 $invoice_data{invoice_lines} = sub {
3550 my $lines = shift || scalar(@buf);
3562 push @collect, split("\n",
3563 $text_template->fill_in( HASH => \%invoice_data )
3565 $invoice_data{'page'}++;
3567 map "$_\n", @collect;
3569 # this is where we actually create the invoice
3570 warn "filling in template for invoice ". $self->invnum. "\n"
3572 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3575 $text_template->fill_in(HASH => \%invoice_data);
3579 # helper routine for generating date ranges
3580 sub _prior_month30s {
3583 [ 1, 2592000 ], # 0-30 days ago
3584 [ 2592000, 5184000 ], # 30-60 days ago
3585 [ 5184000, 7776000 ], # 60-90 days ago
3586 [ 7776000, 0 ], # 90+ days ago
3589 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3590 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3595 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3597 Returns an postscript invoice, as a scalar.
3599 Options can be passed as a hashref (recommended) or as a list of time, template
3600 and then any key/value pairs for any other options.
3602 I<time> an optional value used to control the printing of overdue messages. The
3603 default is now. It isn't the date of the invoice; that's the `_date' field.
3604 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3605 L<Time::Local> and L<Date::Parse> for conversion functions.
3607 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3614 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3615 my $ps = generate_ps($file);
3617 unlink($barcodefile) if $barcodefile;
3622 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3624 Returns an PDF invoice, as a scalar.
3626 Options can be passed as a hashref (recommended) or as a list of time, template
3627 and then any key/value pairs for any other options.
3629 I<time> an optional value used to control the printing of overdue messages. The
3630 default is now. It isn't the date of the invoice; that's the `_date' field.
3631 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3632 L<Time::Local> and L<Date::Parse> for conversion functions.
3634 I<template>, if specified, is the name of a suffix for alternate invoices.
3636 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3643 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3644 my $pdf = generate_pdf($file);
3646 unlink($barcodefile) if $barcodefile;
3651 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3653 Returns an HTML invoice, as a scalar.
3655 I<time> an optional value used to control the printing of overdue messages. The
3656 default is now. It isn't the date of the invoice; that's the `_date' field.
3657 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3658 L<Time::Local> and L<Date::Parse> for conversion functions.
3660 I<template>, if specified, is the name of a suffix for alternate invoices.
3662 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3664 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3665 when emailing the invoice as part of a multipart/related MIME email.
3673 %params = %{ shift() };
3675 $params{'time'} = shift;
3676 $params{'template'} = shift;
3677 $params{'cid'} = shift;
3680 $params{'format'} = 'html';
3682 $self->print_generic( %params );
3685 # quick subroutine for print_latex
3687 # There are ten characters that LaTeX treats as special characters, which
3688 # means that they do not simply typeset themselves:
3689 # # $ % & ~ _ ^ \ { }
3691 # TeX ignores blanks following an escaped character; if you want a blank (as
3692 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3696 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3697 $value =~ s/([<>])/\$$1\$/g;
3703 encode_entities($value);
3707 sub _html_escape_nbsp {
3708 my $value = _html_escape(shift);
3709 $value =~ s/ +/ /g;
3713 #utility methods for print_*
3715 sub _translate_old_latex_format {
3716 warn "_translate_old_latex_format called\n"
3723 if ( $line =~ /^%%Detail\s*$/ ) {
3725 push @template, q![@--!,
3726 q! foreach my $_tr_line (@detail_items) {!,
3727 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3728 q! $_tr_line->{'description'} .= !,
3729 q! "\\tabularnewline\n~~".!,
3730 q! join( "\\tabularnewline\n~~",!,
3731 q! @{$_tr_line->{'ext_description'}}!,
3735 while ( ( my $line_item_line = shift )
3736 !~ /^%%EndDetail\s*$/ ) {
3737 $line_item_line =~ s/'/\\'/g; # nice LTS
3738 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3739 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3740 push @template, " \$OUT .= '$line_item_line';";
3743 push @template, '}',
3746 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3748 push @template, '[@--',
3749 ' foreach my $_tr_line (@total_items) {';
3751 while ( ( my $total_item_line = shift )
3752 !~ /^%%EndTotalDetails\s*$/ ) {
3753 $total_item_line =~ s/'/\\'/g; # nice LTS
3754 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3755 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3756 push @template, " \$OUT .= '$total_item_line';";
3759 push @template, '}',
3763 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3764 push @template, $line;
3770 warn "$_\n" foreach @template;
3778 my $conf = $self->conf;
3780 #check for an invoice-specific override
3781 return $self->invoice_terms if $self->invoice_terms;
3783 #check for a customer- specific override
3784 my $cust_main = $self->cust_main;
3785 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3787 #use configured default
3788 $conf->config('invoice_default_terms') || '';
3794 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3795 $duedate = $self->_date() + ( $1 * 86400 );
3802 $self->due_date ? time2str(shift, $self->due_date) : '';
3805 sub balance_due_msg {
3807 my $msg = $self->mt('Balance Due');
3808 return $msg unless $self->terms;
3809 if ( $self->due_date ) {
3810 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3811 $self->due_date2str($date_format);
3812 } elsif ( $self->terms ) {
3813 $msg .= ' - '. $self->terms;
3818 sub balance_due_date {
3820 my $conf = $self->conf;
3822 if ( $conf->exists('invoice_default_terms')
3823 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3824 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3829 sub credit_balance_msg {
3831 $self->mt('Credit Balance Remaining')
3834 =item invnum_date_pretty
3836 Returns a string with the invoice number and date, for example:
3837 "Invoice #54 (3/20/2008)"
3841 sub invnum_date_pretty {
3843 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3848 Returns a string with the date, for example: "3/20/2008"
3854 time2str($date_format, $self->_date);
3857 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3859 Generate section information for all items appearing on this invoice.
3860 This will only be called for multi-section invoices.
3862 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3863 related display records (L<FS::cust_bill_pkg_display>) and organize
3864 them into two groups ("early" and "late" according to whether they come
3865 before or after the total), then into sections. A subtotal is calculated
3868 Section descriptions are returned in sort weight order. Each consists
3869 of a hash containing:
3871 description: the package category name, escaped
3872 subtotal: the total charges in that section
3873 tax_section: a flag indicating that the section contains only tax charges
3874 summarized: same as tax_section, for some reason
3875 sort_weight: the package category's sort weight
3877 If 'condense' is set on the display record, it also contains everything
3878 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3879 coderefs to generate parts of the invoice. This is not advised.
3883 LATE: an arrayref to push the "late" section hashes onto. The "early"
3884 group is simply returned from the method.
3886 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3887 Turning this on has the following effects:
3888 - Ignores display items with the 'summary' flag.
3889 - Combines all items into the "early" group.
3890 - Creates sections for all non-disabled package categories, even if they
3891 have no charges on this invoice, as well as a section with no name.
3893 ESCAPE: an escape function to use for section titles.
3895 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3896 sorted list. If there are any of these, section subtotals exclude
3899 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3900 passed through to C<_condense_section()>.
3904 use vars qw(%pkg_category_cache);
3905 sub _items_sections {
3908 my $summarypage = shift;
3910 my $extra_sections = shift;
3914 my %late_subtotal = ();
3917 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3920 my $usage = $cust_bill_pkg->usage;
3922 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3923 next if ( $display->summary && $summarypage );
3925 my $section = $display->section;
3926 my $type = $display->type;
3928 $not_tax{$section} = 1
3929 unless $cust_bill_pkg->pkgnum == 0;
3931 if ( $display->post_total && !$summarypage ) {
3932 if (! $type || $type eq 'S') {
3933 $late_subtotal{$section} += $cust_bill_pkg->setup
3934 if $cust_bill_pkg->setup != 0
3935 || $cust_bill_pkg->setup_show_zero;
3939 $late_subtotal{$section} += $cust_bill_pkg->recur
3940 if $cust_bill_pkg->recur != 0
3941 || $cust_bill_pkg->recur_show_zero;
3944 if ($type && $type eq 'R') {
3945 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3946 if $cust_bill_pkg->recur != 0
3947 || $cust_bill_pkg->recur_show_zero;
3950 if ($type && $type eq 'U') {
3951 $late_subtotal{$section} += $usage
3952 unless scalar(@$extra_sections);
3957 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3959 if (! $type || $type eq 'S') {
3960 $subtotal{$section} += $cust_bill_pkg->setup
3961 if $cust_bill_pkg->setup != 0
3962 || $cust_bill_pkg->setup_show_zero;
3966 $subtotal{$section} += $cust_bill_pkg->recur
3967 if $cust_bill_pkg->recur != 0
3968 || $cust_bill_pkg->recur_show_zero;
3971 if ($type && $type eq 'R') {
3972 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3973 if $cust_bill_pkg->recur != 0
3974 || $cust_bill_pkg->recur_show_zero;
3977 if ($type && $type eq 'U') {
3978 $subtotal{$section} += $usage
3979 unless scalar(@$extra_sections);
3988 %pkg_category_cache = ();
3990 push @$late, map { { 'description' => &{$escape}($_),
3991 'subtotal' => $late_subtotal{$_},
3993 'sort_weight' => ( _pkg_category($_)
3994 ? _pkg_category($_)->weight
3997 ((_pkg_category($_) && _pkg_category($_)->condense)
3998 ? $self->_condense_section($format)
4002 sort _sectionsort keys %late_subtotal;
4005 if ( $summarypage ) {
4006 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4007 map { $_->categoryname } qsearch('pkg_category', {});
4008 push @sections, '' if exists($subtotal{''});
4010 @sections = keys %subtotal;
4013 my @early = map { { 'description' => &{$escape}($_),
4014 'subtotal' => $subtotal{$_},
4015 'summarized' => $not_tax{$_} ? '' : 'Y',
4016 'tax_section' => $not_tax{$_} ? '' : 'Y',
4017 'sort_weight' => ( _pkg_category($_)
4018 ? _pkg_category($_)->weight
4021 ((_pkg_category($_) && _pkg_category($_)->condense)
4022 ? $self->_condense_section($format)
4027 push @early, @$extra_sections if $extra_sections;
4029 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4033 #helper subs for above
4036 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4040 my $categoryname = shift;
4041 $pkg_category_cache{$categoryname} ||=
4042 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4045 my %condensed_format = (
4046 'label' => [ qw( Description Qty Amount ) ],
4048 sub { shift->{description} },
4049 sub { shift->{quantity} },
4050 sub { my($href, %opt) = @_;
4051 ($opt{dollar} || ''). $href->{amount};
4054 'align' => [ qw( l r r ) ],
4055 'span' => [ qw( 5 1 1 ) ], # unitprices?
4056 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4059 sub _condense_section {
4060 my ( $self, $format ) = ( shift, shift );
4062 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4063 qw( description_generator
4066 total_line_generator
4071 sub _condensed_generator_defaults {
4072 my ( $self, $format ) = ( shift, shift );
4073 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4082 sub _condensed_header_generator {
4083 my ( $self, $format ) = ( shift, shift );
4085 my ( $f, $prefix, $suffix, $separator, $column ) =
4086 _condensed_generator_defaults($format);
4088 if ($format eq 'latex') {
4089 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4090 $suffix = "\\\\\n\\hline";
4093 sub { my ($d,$a,$s,$w) = @_;
4094 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4096 } elsif ( $format eq 'html' ) {
4097 $prefix = '<th></th>';
4101 sub { my ($d,$a,$s,$w) = @_;
4102 return qq!<th align="$html_align{$a}">$d</th>!;
4110 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4112 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4115 $prefix. join($separator, @result). $suffix;
4120 sub _condensed_description_generator {
4121 my ( $self, $format ) = ( shift, shift );
4123 my ( $f, $prefix, $suffix, $separator, $column ) =
4124 _condensed_generator_defaults($format);
4126 my $money_char = '$';
4127 if ($format eq 'latex') {
4128 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4130 $separator = " & \n";
4132 sub { my ($d,$a,$s,$w) = @_;
4133 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4135 $money_char = '\\dollar';
4136 }elsif ( $format eq 'html' ) {
4137 $prefix = '"><td align="center"></td>';
4141 sub { my ($d,$a,$s,$w) = @_;
4142 return qq!<td align="$html_align{$a}">$d</td>!;
4144 #$money_char = $conf->config('money_char') || '$';
4145 $money_char = ''; # this is madness
4153 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4155 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4157 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4158 map { $f->{$_}->[$i] } qw(align span width)
4162 $prefix. join( $separator, @result ). $suffix;
4167 sub _condensed_total_generator {
4168 my ( $self, $format ) = ( shift, shift );
4170 my ( $f, $prefix, $suffix, $separator, $column ) =
4171 _condensed_generator_defaults($format);
4174 if ($format eq 'latex') {
4177 $separator = " & \n";
4179 sub { my ($d,$a,$s,$w) = @_;
4180 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4182 }elsif ( $format eq 'html' ) {
4186 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4188 sub { my ($d,$a,$s,$w) = @_;
4189 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4198 # my $r = &{$f->{fields}->[$i]}(@args);
4199 # $r .= ' Total' unless $i;
4201 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4203 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4204 map { $f->{$_}->[$i] } qw(align span width)
4208 $prefix. join( $separator, @result ). $suffix;
4213 =item total_line_generator FORMAT
4215 Returns a coderef used for generation of invoice total line items for this
4216 usage_class. FORMAT is either html or latex
4220 # should not be used: will have issues with hash element names (description vs
4221 # total_item and amount vs total_amount -- another array of functions?
4223 sub _condensed_total_line_generator {
4224 my ( $self, $format ) = ( shift, shift );
4226 my ( $f, $prefix, $suffix, $separator, $column ) =
4227 _condensed_generator_defaults($format);
4230 if ($format eq 'latex') {
4233 $separator = " & \n";
4235 sub { my ($d,$a,$s,$w) = @_;
4236 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4238 }elsif ( $format eq 'html' ) {
4242 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4244 sub { my ($d,$a,$s,$w) = @_;
4245 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4254 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4256 &{$column}( &{$f->{fields}->[$i]}(@args),
4257 map { $f->{$_}->[$i] } qw(align span width)
4261 $prefix. join( $separator, @result ). $suffix;
4266 #sub _items_extra_usage_sections {
4268 # my $escape = shift;
4270 # my %sections = ();
4272 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4273 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4275 # next unless $cust_bill_pkg->pkgnum > 0;
4277 # foreach my $section ( keys %usage_class ) {
4279 # my $usage = $cust_bill_pkg->usage($section);
4281 # next unless $usage && $usage > 0;
4283 # $sections{$section} ||= 0;
4284 # $sections{$section} += $usage;
4290 # map { { 'description' => &{$escape}($_),
4291 # 'subtotal' => $sections{$_},
4292 # 'summarized' => '',
4293 # 'tax_section' => '',
4296 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4300 sub _items_extra_usage_sections {
4302 my $conf = $self->conf;
4310 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4312 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4313 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4314 next unless $cust_bill_pkg->pkgnum > 0;
4316 foreach my $classnum ( keys %usage_class ) {
4317 my $section = $usage_class{$classnum}->classname;
4318 $classnums{$section} = $classnum;
4320 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4321 my $amount = $detail->amount;
4322 next unless $amount && $amount > 0;
4324 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4325 $sections{$section}{amount} += $amount; #subtotal
4326 $sections{$section}{calls}++;
4327 $sections{$section}{duration} += $detail->duration;
4329 my $desc = $detail->regionname;
4330 my $description = $desc;
4331 $description = substr($desc, 0, $maxlength). '...'
4332 if $format eq 'latex' && length($desc) > $maxlength;
4334 $lines{$section}{$desc} ||= {
4335 description => &{$escape}($description),
4336 #pkgpart => $part_pkg->pkgpart,
4337 pkgnum => $cust_bill_pkg->pkgnum,
4342 #unit_amount => $cust_bill_pkg->unitrecur,
4343 quantity => $cust_bill_pkg->quantity,
4344 product_code => 'N/A',
4345 ext_description => [],
4348 $lines{$section}{$desc}{amount} += $amount;
4349 $lines{$section}{$desc}{calls}++;
4350 $lines{$section}{$desc}{duration} += $detail->duration;
4356 my %sectionmap = ();
4357 foreach (keys %sections) {
4358 my $usage_class = $usage_class{$classnums{$_}};
4359 $sectionmap{$_} = { 'description' => &{$escape}($_),
4360 'amount' => $sections{$_}{amount}, #subtotal
4361 'calls' => $sections{$_}{calls},
4362 'duration' => $sections{$_}{duration},
4364 'tax_section' => '',
4365 'sort_weight' => $usage_class->weight,
4366 ( $usage_class->format
4367 ? ( map { $_ => $usage_class->$_($format) }
4368 qw( description_generator header_generator total_generator total_line_generator )
4375 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4379 foreach my $section ( keys %lines ) {
4380 foreach my $line ( keys %{$lines{$section}} ) {
4381 my $l = $lines{$section}{$line};
4382 $l->{section} = $sectionmap{$section};
4383 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4384 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4389 return(\@sections, \@lines);
4395 my $end = $self->_date;
4397 # start at date of previous invoice + 1 second or 0 if no previous invoice
4398 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4399 $start = 0 if !$start;
4402 my $cust_main = $self->cust_main;
4403 my @pkgs = $cust_main->all_pkgs;
4404 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4407 foreach my $pkg ( @pkgs ) {
4408 my @h_cust_svc = $pkg->h_cust_svc($end);
4409 foreach my $h_cust_svc ( @h_cust_svc ) {
4410 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4411 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4413 my $inserted = $h_cust_svc->date_inserted;
4414 my $deleted = $h_cust_svc->date_deleted;
4415 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4417 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4419 # DID either activated or ported in; cannot be both for same DID simultaneously
4420 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4421 && (!$phone_inserted->lnp_status
4422 || $phone_inserted->lnp_status eq ''
4423 || $phone_inserted->lnp_status eq 'native')) {
4426 else { # this one not so clean, should probably move to (h_)svc_phone
4427 my $phone_portedin = qsearchs( 'h_svc_phone',
4428 { 'svcnum' => $h_cust_svc->svcnum,
4429 'lnp_status' => 'portedin' },
4430 FS::h_svc_phone->sql_h_searchs($end),
4432 $num_portedin++ if $phone_portedin;
4435 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4436 if($deleted >= $start && $deleted <= $end && $phone_deleted
4437 && (!$phone_deleted->lnp_status
4438 || $phone_deleted->lnp_status ne 'portingout')) {
4441 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4442 && $phone_deleted->lnp_status
4443 && $phone_deleted->lnp_status eq 'portingout') {
4447 # increment usage minutes
4448 if ( $phone_inserted ) {
4449 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4450 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4453 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4456 # don't look at this service again
4457 push @seen, $h_cust_svc->svcnum;
4461 $minutes = sprintf("%d", $minutes);
4462 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4463 . "$num_deactivated Ported-Out: $num_portedout ",
4464 "Total Minutes: $minutes");
4467 sub _items_accountcode_cdr {
4472 my $section = { 'amount' => 0,
4475 'sort_weight' => '',
4477 'description' => 'Usage by Account Code',
4483 my %accountcodes = ();
4485 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4486 next unless $cust_bill_pkg->pkgnum > 0;
4488 my @header = $cust_bill_pkg->details_header;
4489 next unless scalar(@header);
4490 $section->{'header'} = join(',',@header);
4492 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4494 $section->{'header'} = $detail->formatted('format' => $format)
4495 if($detail->detail eq $section->{'header'});
4497 my $accountcode = $detail->accountcode;
4498 next unless $accountcode;
4500 my $amount = $detail->amount;
4501 next unless $amount && $amount > 0;
4503 $accountcodes{$accountcode} ||= {
4504 description => $accountcode,
4511 product_code => 'N/A',
4512 section => $section,
4513 ext_description => [ $section->{'header'} ],
4517 $section->{'amount'} += $amount;
4518 $accountcodes{$accountcode}{'amount'} += $amount;
4519 $accountcodes{$accountcode}{calls}++;
4520 $accountcodes{$accountcode}{duration} += $detail->duration;
4521 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4525 foreach my $l ( values %accountcodes ) {
4526 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4527 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4528 foreach my $sorted_detail ( @sorted_detail ) {
4529 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4531 delete $l->{detail_temp};
4535 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4537 return ($section,\@sorted_lines);
4540 sub _items_svc_phone_sections {
4542 my $conf = $self->conf;
4550 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4552 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4553 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4555 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4556 next unless $cust_bill_pkg->pkgnum > 0;
4558 my @header = $cust_bill_pkg->details_header;
4559 next unless scalar(@header);
4561 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4563 my $phonenum = $detail->phonenum;
4564 next unless $phonenum;
4566 my $amount = $detail->amount;
4567 next unless $amount && $amount > 0;
4569 $sections{$phonenum} ||= { 'amount' => 0,
4572 'sort_weight' => -1,
4573 'phonenum' => $phonenum,
4575 $sections{$phonenum}{amount} += $amount; #subtotal
4576 $sections{$phonenum}{calls}++;
4577 $sections{$phonenum}{duration} += $detail->duration;
4579 my $desc = $detail->regionname;
4580 my $description = $desc;
4581 $description = substr($desc, 0, $maxlength). '...'
4582 if $format eq 'latex' && length($desc) > $maxlength;
4584 $lines{$phonenum}{$desc} ||= {
4585 description => &{$escape}($description),
4586 #pkgpart => $part_pkg->pkgpart,
4594 product_code => 'N/A',
4595 ext_description => [],
4598 $lines{$phonenum}{$desc}{amount} += $amount;
4599 $lines{$phonenum}{$desc}{calls}++;
4600 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4602 my $line = $usage_class{$detail->classnum}->classname;
4603 $sections{"$phonenum $line"} ||=
4607 'sort_weight' => $usage_class{$detail->classnum}->weight,
4608 'phonenum' => $phonenum,
4609 'header' => [ @header ],
4611 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4612 $sections{"$phonenum $line"}{calls}++;
4613 $sections{"$phonenum $line"}{duration} += $detail->duration;
4615 $lines{"$phonenum $line"}{$desc} ||= {
4616 description => &{$escape}($description),
4617 #pkgpart => $part_pkg->pkgpart,
4625 product_code => 'N/A',
4626 ext_description => [],
4629 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4630 $lines{"$phonenum $line"}{$desc}{calls}++;
4631 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4632 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4633 $detail->formatted('format' => $format);
4638 my %sectionmap = ();
4639 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4640 foreach ( keys %sections ) {
4641 my @header = @{ $sections{$_}{header} || [] };
4643 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4644 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4645 my $usage_class = $summary ? $simple : $usage_simple;
4646 my $ending = $summary ? ' usage charges' : '';
4649 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4651 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4652 'amount' => $sections{$_}{amount}, #subtotal
4653 'calls' => $sections{$_}{calls},
4654 'duration' => $sections{$_}{duration},
4656 'tax_section' => '',
4657 'phonenum' => $sections{$_}{phonenum},
4658 'sort_weight' => $sections{$_}{sort_weight},
4659 'post_total' => $summary, #inspire pagebreak
4661 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4662 qw( description_generator
4665 total_line_generator
4672 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4673 $a->{sort_weight} <=> $b->{sort_weight}
4678 foreach my $section ( keys %lines ) {
4679 foreach my $line ( keys %{$lines{$section}} ) {
4680 my $l = $lines{$section}{$line};
4681 $l->{section} = $sectionmap{$section};
4682 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4683 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4688 if($conf->exists('phone_usage_class_summary')) {
4689 # this only works with Latex
4693 # after this, we'll have only two sections per DID:
4694 # Calls Summary and Calls Detail
4695 foreach my $section ( @sections ) {
4696 if($section->{'post_total'}) {
4697 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4698 $section->{'total_line_generator'} = sub { '' };
4699 $section->{'total_generator'} = sub { '' };
4700 $section->{'header_generator'} = sub { '' };
4701 $section->{'description_generator'} = '';
4702 push @newsections, $section;
4703 my %calls_detail = %$section;
4704 $calls_detail{'post_total'} = '';
4705 $calls_detail{'sort_weight'} = '';
4706 $calls_detail{'description_generator'} = sub { '' };
4707 $calls_detail{'header_generator'} = sub {
4708 return ' & Date/Time & Called Number & Duration & Price'
4709 if $format eq 'latex';
4712 $calls_detail{'description'} = 'Calls Detail: '
4713 . $section->{'phonenum'};
4714 push @newsections, \%calls_detail;
4718 # after this, each usage class is collapsed/summarized into a single
4719 # line under the Calls Summary section
4720 foreach my $newsection ( @newsections ) {
4721 if($newsection->{'post_total'}) { # this means Calls Summary
4722 foreach my $section ( @sections ) {
4723 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4724 && !$section->{'post_total'});
4725 my $newdesc = $section->{'description'};
4726 my $tn = $section->{'phonenum'};
4727 $newdesc =~ s/$tn//g;
4728 my $line = { ext_description => [],
4732 calls => $section->{'calls'},
4733 section => $newsection,
4734 duration => $section->{'duration'},
4735 description => $newdesc,
4736 amount => sprintf("%.2f",$section->{'amount'}),
4737 product_code => 'N/A',
4739 push @newlines, $line;
4744 # after this, Calls Details is populated with all CDRs
4745 foreach my $newsection ( @newsections ) {
4746 if(!$newsection->{'post_total'}) { # this means Calls Details
4747 foreach my $line ( @lines ) {
4748 next unless (scalar(@{$line->{'ext_description'}}) &&
4749 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4751 my @extdesc = @{$line->{'ext_description'}};
4753 foreach my $extdesc ( @extdesc ) {
4754 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4755 push @newextdesc, $extdesc;
4757 $line->{'ext_description'} = \@newextdesc;
4758 $line->{'section'} = $newsection;
4759 push @newlines, $line;
4764 return(\@newsections, \@newlines);
4767 return(\@sections, \@lines);
4771 sub _items { # seems to be unused
4774 #my @display = scalar(@_)
4776 # : qw( _items_previous _items_pkg );
4777 # #: qw( _items_pkg );
4778 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4779 my @display = qw( _items_previous _items_pkg );
4782 foreach my $display ( @display ) {
4783 push @b, $self->$display(@_);
4788 sub _items_previous {
4790 my $conf = $self->conf;
4791 my $cust_main = $self->cust_main;
4792 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4794 foreach ( @pr_cust_bill ) {
4795 my $date = $conf->exists('invoice_show_prior_due_date')
4796 ? 'due '. $_->due_date2str($date_format)
4797 : time2str($date_format, $_->_date);
4799 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4800 #'pkgpart' => 'N/A',
4802 'amount' => sprintf("%.2f", $_->owed),
4808 # 'description' => 'Previous Balance',
4809 # #'pkgpart' => 'N/A',
4810 # 'pkgnum' => 'N/A',
4811 # 'amount' => sprintf("%10.2f", $pr_total ),
4812 # 'ext_description' => [ map {
4813 # "Invoice ". $_->invnum.
4814 # " (". time2str("%x",$_->_date). ") ".
4815 # sprintf("%10.2f", $_->owed)
4816 # } @pr_cust_bill ],
4821 =item _items_pkg [ OPTIONS ]
4823 Return line item hashes for each package item on this invoice. Nearly
4826 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4828 The only OPTIONS accepted is 'section', which may point to a hashref
4829 with a key named 'condensed', which may have a true value. If it
4830 does, this method tries to merge identical items into items with
4831 'quantity' equal to the number of items (not the sum of their
4832 separate quantities, for some reason).
4840 warn "$me _items_pkg searching for all package line items\n"
4843 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4845 warn "$me _items_pkg filtering line items\n"
4847 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4849 if ($options{section} && $options{section}->{condensed}) {
4851 warn "$me _items_pkg condensing section\n"
4855 local $Storable::canonical = 1;
4856 foreach ( @items ) {
4858 delete $item->{ref};
4859 delete $item->{ext_description};
4860 my $key = freeze($item);
4861 $itemshash{$key} ||= 0;
4862 $itemshash{$key} ++; # += $item->{quantity};
4864 @items = sort { $a->{description} cmp $b->{description} }
4865 map { my $i = thaw($_);
4866 $i->{quantity} = $itemshash{$_};
4868 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4874 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4881 return 0 unless $a->itemdesc cmp $b->itemdesc;
4882 return -1 if $b->itemdesc eq 'Tax';
4883 return 1 if $a->itemdesc eq 'Tax';
4884 return -1 if $b->itemdesc eq 'Other surcharges';
4885 return 1 if $a->itemdesc eq 'Other surcharges';
4886 $a->itemdesc cmp $b->itemdesc;
4891 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4892 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4895 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4897 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4898 list of hashrefs describing the line items they generate on the invoice.
4900 OPTIONS may include:
4902 format: the invoice format.
4904 escape_function: the function used to escape strings.
4906 DEPRECATED? (expensive, mostly unused?)
4907 format_function: the function used to format CDRs.
4909 section: a hashref containing 'description'; if this is present,
4910 cust_bill_pkg_display records not belonging to this section are
4913 multisection: a flag indicating that this is a multisection invoice,
4914 which does something complicated.
4916 multilocation: a flag to display the location label for the package.
4918 Returns a list of hashrefs, each of which may contain:
4920 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4921 ext_description, which is an arrayref of detail lines to show below
4926 sub _items_cust_bill_pkg {
4928 my $conf = $self->conf;
4929 my $cust_bill_pkgs = shift;
4932 my $format = $opt{format} || '';
4933 my $escape_function = $opt{escape_function} || sub { shift };
4934 my $format_function = $opt{format_function} || '';
4935 my $no_usage = $opt{no_usage} || '';
4936 my $unsquelched = $opt{unsquelched} || ''; #unused
4937 my $section = $opt{section}->{description} if $opt{section};
4938 my $summary_page = $opt{summary_page} || ''; #unused
4939 my $multilocation = $opt{multilocation} || '';
4940 my $multisection = $opt{multisection} || '';
4941 my $discount_show_always = 0;
4943 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4945 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4948 my ($s, $r, $u) = ( undef, undef, undef );
4949 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4952 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4953 if ( $_ && !$cust_bill_pkg->hidden ) {
4954 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4955 $_->{amount} =~ s/^\-0\.00$/0.00/;
4956 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4958 if $_->{amount} != 0
4959 || $discount_show_always
4960 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4961 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4967 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4969 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4970 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4973 foreach my $display ( grep { defined($section)
4974 ? $_->section eq $section
4977 #grep { !$_->summary || !$summary_page } # bunk!
4978 grep { !$_->summary || $multisection }
4979 @cust_bill_pkg_display
4983 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4984 $display->billpkgdisplaynum. "\n"
4987 my $type = $display->type;
4989 my $desc = $cust_bill_pkg->desc;
4990 $desc = substr($desc, 0, $maxlength). '...'
4991 if $format eq 'latex' && length($desc) > $maxlength;
4993 my %details_opt = ( 'format' => $format,
4994 'escape_function' => $escape_function,
4995 'format_function' => $format_function,
4996 'no_usage' => $opt{'no_usage'},
4999 if ( $cust_bill_pkg->pkgnum > 0 ) {
5001 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5004 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5006 # which pkgpart to show for display purposes?
5007 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5009 # start/end dates for invoice formats that do nonstandard
5011 my %item_dates = ();
5012 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5013 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5015 if ( (!$type || $type eq 'S')
5016 && ( $cust_bill_pkg->setup != 0
5017 || $cust_bill_pkg->setup_show_zero
5022 warn "$me _items_cust_bill_pkg adding setup\n"
5025 my $description = $desc;
5026 $description .= ' Setup'
5027 if $cust_bill_pkg->recur != 0
5028 || $discount_show_always
5029 || $cust_bill_pkg->recur_show_zero;
5033 unless ( $cust_pkg->part_pkg->hide_svc_detail
5034 || $cust_bill_pkg->hidden )
5037 my @svc_labels = map &{$escape_function}($_),
5038 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5039 push @d, @svc_labels
5040 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5041 $svc_label = $svc_labels[0];
5043 if ( $multilocation ) {
5044 my $loc = $cust_pkg->location_label;
5045 $loc = substr($loc, 0, $maxlength). '...'
5046 if $format eq 'latex' && length($loc) > $maxlength;
5047 push @d, &{$escape_function}($loc);
5050 } #unless hiding service details
5052 push @d, $cust_bill_pkg->details(%details_opt)
5053 if $cust_bill_pkg->recur == 0;
5055 if ( $cust_bill_pkg->hidden ) {
5056 $s->{amount} += $cust_bill_pkg->setup;
5057 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5058 push @{ $s->{ext_description} }, @d;
5062 description => $description,
5063 pkgpart => $pkgpart,
5064 pkgnum => $cust_bill_pkg->pkgnum,
5065 amount => $cust_bill_pkg->setup,
5066 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5067 unit_amount => $cust_bill_pkg->unitsetup,
5068 quantity => $cust_bill_pkg->quantity,
5069 ext_description => \@d,
5070 svc_label => ($svc_label || ''),
5076 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5078 $cust_bill_pkg->recur != 0
5079 || $cust_bill_pkg->setup == 0
5080 || $discount_show_always
5081 || $cust_bill_pkg->recur_show_zero
5086 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5089 my $is_summary = $display->summary;
5090 my $description = ($is_summary && $type && $type eq 'U')
5091 ? "Usage charges" : $desc;
5093 my $part_pkg = $cust_pkg->part_pkg;
5095 #pry be a bit more efficient to look some of this conf stuff up
5098 $conf->exists('disable_line_item_date_ranges')
5099 || $part_pkg->option('disable_line_item_date_ranges',1)
5100 || ! $cust_bill_pkg->sdate
5101 || ! $cust_bill_pkg->edate
5104 my $date_style = '';
5105 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5106 $cust_main->agentnum
5108 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5109 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5110 $cust_main->agentnum
5112 if ( defined($date_style) && $date_style eq 'month_of' ) {
5113 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5114 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5115 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5116 $cust_main->agentnum
5118 $desc .= ' ' unless $desc =~ /\s$/;
5119 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5121 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5122 " - ". time2str($date_format, $cust_bill_pkg->edate);
5124 $description .= " ($time_period)";
5128 my @seconds = (); # for display of usage info
5131 #at least until cust_bill_pkg has "past" ranges in addition to
5132 #the "future" sdate/edate ones... see #3032
5133 my @dates = ( $self->_date );
5134 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5135 push @dates, $prev->sdate if $prev;
5136 push @dates, undef if !$prev;
5138 unless ( $cust_pkg->part_pkg->hide_svc_detail
5139 || $cust_bill_pkg->itemdesc
5140 || $cust_bill_pkg->hidden
5141 || $is_summary && $type && $type eq 'U' )
5144 warn "$me _items_cust_bill_pkg adding service details\n"
5147 my @svc_labels = map &{$escape_function}($_),
5148 $cust_pkg->h_labels_short(@dates, 'I');
5149 push @d, @svc_labels
5150 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5151 $svc_label = $svc_labels[0];
5153 warn "$me _items_cust_bill_pkg done adding service details\n"
5156 if ( $multilocation ) {
5157 my $loc = $cust_pkg->location_label;
5158 $loc = substr($loc, 0, $maxlength). '...'
5159 if $format eq 'latex' && length($loc) > $maxlength;
5160 push @d, &{$escape_function}($loc);
5163 # Display of seconds_since_sqlradacct:
5164 # On the invoice, when processing @detail_items, look for a field
5165 # named 'seconds'. This will contain total seconds for each
5166 # service, in the same order as @ext_description. For services
5167 # that don't support this it will show undef.
5168 if ( $conf->exists('svc_acct-usage_seconds')
5169 and ! $cust_bill_pkg->pkgpart_override ) {
5170 foreach my $cust_svc (
5171 $cust_pkg->h_cust_svc(@dates, 'I')
5174 # eval because not having any part_export_usage exports
5175 # is a fatal error, last_bill/_date because that's how
5176 # sqlradius_hour billing does it
5178 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5180 push @seconds, $sec;
5182 } #if svc_acct-usage_seconds
5186 unless ( $is_summary ) {
5187 warn "$me _items_cust_bill_pkg adding details\n"
5190 #instead of omitting details entirely in this case (unwanted side
5191 # effects), just omit CDRs
5192 $details_opt{'no_usage'} = 1
5193 if $type && $type eq 'R';
5195 push @d, $cust_bill_pkg->details(%details_opt);
5198 warn "$me _items_cust_bill_pkg calculating amount\n"
5203 $amount = $cust_bill_pkg->recur;
5204 } elsif ($type eq 'R') {
5205 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5206 } elsif ($type eq 'U') {
5207 $amount = $cust_bill_pkg->usage;
5210 if ( !$type || $type eq 'R' ) {
5212 warn "$me _items_cust_bill_pkg adding recur\n"
5215 if ( $cust_bill_pkg->hidden ) {
5216 $r->{amount} += $amount;
5217 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5218 push @{ $r->{ext_description} }, @d;
5221 description => $description,
5222 pkgpart => $pkgpart,
5223 pkgnum => $cust_bill_pkg->pkgnum,
5225 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5226 unit_amount => $cust_bill_pkg->unitrecur,
5227 quantity => $cust_bill_pkg->quantity,
5229 ext_description => \@d,
5230 svc_label => ($svc_label || ''),
5232 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5235 } else { # $type eq 'U'
5237 warn "$me _items_cust_bill_pkg adding usage\n"
5240 if ( $cust_bill_pkg->hidden ) {
5241 $u->{amount} += $amount;
5242 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5243 push @{ $u->{ext_description} }, @d;
5246 description => $description,
5247 pkgpart => $pkgpart,
5248 pkgnum => $cust_bill_pkg->pkgnum,
5250 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5251 unit_amount => $cust_bill_pkg->unitrecur,
5252 quantity => $cust_bill_pkg->quantity,
5254 ext_description => \@d,
5259 } # recurring or usage with recurring charge
5261 } else { #pkgnum tax or one-shot line item (??)
5263 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5266 if ( $cust_bill_pkg->setup != 0 ) {
5268 'description' => $desc,
5269 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5272 if ( $cust_bill_pkg->recur != 0 ) {
5274 'description' => "$desc (".
5275 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5276 time2str($date_format, $cust_bill_pkg->edate). ')',
5277 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5285 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5286 && $conf->exists('discount-show-always'));
5290 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5292 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5293 $_->{amount} =~ s/^\-0\.00$/0.00/;
5294 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5296 if $_->{amount} != 0
5297 || $discount_show_always
5298 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5299 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5303 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5310 sub _items_credits {
5311 my( $self, %opt ) = @_;
5312 my $trim_len = $opt{'trim_len'} || 60;
5316 foreach ( $self->cust_credited ) {
5318 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5320 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5321 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5322 $reason = " ($reason) " if $reason;
5325 #'description' => 'Credit ref\#'. $_->crednum.
5326 # " (". time2str("%x",$_->cust_credit->_date) .")".
5328 'description' => $self->mt('Credit applied').' '.
5329 time2str($date_format,$_->cust_credit->_date). $reason,
5330 'amount' => sprintf("%.2f",$_->amount),
5338 sub _items_payments {
5342 #get & print payments
5343 foreach ( $self->cust_bill_pay ) {
5345 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5347 my $desc = $self->mt('Payment received').' '.
5348 time2str($date_format,$_->cust_pay->_date );
5349 $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
5350 if ( $self->conf->exists('invoice_payment_details') );
5353 'description' => $desc,
5354 'amount' => sprintf("%.2f", $_->amount )
5363 =item _items_discounts_avail
5365 Returns an array of line item hashrefs representing available term discounts
5366 for this invoice. This makes the same assumptions that apply to term
5367 discounts in general: that the package is billed monthly, at a flat rate,
5368 with no usage charges. A prorated first month will be handled, as will
5369 a setup fee if the discount is allowed to apply to setup fees.
5373 sub _items_discounts_avail {
5375 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5377 my %plans = $self->discount_plans;
5379 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5383 my $plan = $plans{$months};
5385 my $term_total = sprintf('%.2f', $plan->discounted_total);
5386 my $percent = sprintf('%.0f',
5387 100 * (1 - $term_total / $plan->base_total) );
5388 my $permonth = sprintf('%.2f', $term_total / $months);
5389 my $detail = $self->mt('discount on item'). ' '.
5390 join(', ', map { "#$_" } $plan->pkgnums)
5393 # discounts for non-integer months don't work anyway
5394 $months = sprintf("%d", $months);
5397 description => $self->mt('Save [_1]% by paying for [_2] months',
5399 amount => $self->mt('[_1] ([_2] per month)',
5400 $term_total, $money_char.$permonth),
5401 ext_description => ($detail || ''),
5404 sort { $b <=> $a } keys %plans;
5408 =item call_details [ OPTION => VALUE ... ]
5410 Returns an array of CSV strings representing the call details for this invoice
5411 The only option available is the boolean prepend_billed_number
5416 my ($self, %opt) = @_;
5418 my $format_function = sub { shift };
5420 if ($opt{prepend_billed_number}) {
5421 $format_function = sub {
5425 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5430 my @details = map { $_->details( 'format_function' => $format_function,
5431 'escape_function' => sub{ return() },
5435 $self->cust_bill_pkg;
5436 my $header = $details[0];
5437 ( $header, grep { $_ ne $header } @details );
5447 =item process_reprint
5451 sub process_reprint {
5452 process_re_X('print', @_);
5455 =item process_reemail
5459 sub process_reemail {
5460 process_re_X('email', @_);
5468 process_re_X('fax', @_);
5476 process_re_X('ftp', @_);
5483 sub process_respool {
5484 process_re_X('spool', @_);
5487 use Storable qw(thaw);
5491 my( $method, $job ) = ( shift, shift );
5492 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5494 my $param = thaw(decode_base64(shift));
5495 warn Dumper($param) if $DEBUG;
5506 my($method, $job, %param ) = @_;
5508 warn "re_X $method for job $job with param:\n".
5509 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5512 #some false laziness w/search/cust_bill.html
5514 my $orderby = 'ORDER BY cust_bill._date';
5516 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5518 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5520 my @cust_bill = qsearch( {
5521 #'select' => "cust_bill.*",
5522 'table' => 'cust_bill',
5523 'addl_from' => $addl_from,
5525 'extra_sql' => $extra_sql,
5526 'order_by' => $orderby,
5530 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5532 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5535 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5536 foreach my $cust_bill ( @cust_bill ) {
5537 $cust_bill->$method();
5539 if ( $job ) { #progressbar foo
5541 if ( time - $min_sec > $last ) {
5542 my $error = $job->update_statustext(
5543 int( 100 * $num / scalar(@cust_bill) )
5545 die $error if $error;
5556 =head1 CLASS METHODS
5562 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5567 my ($class, $start, $end) = @_;
5569 $class->paid_sql($start, $end). ' - '.
5570 $class->credited_sql($start, $end);
5575 Returns an SQL fragment to retreive the net amount (charged minus credited).
5580 my ($class, $start, $end) = @_;
5581 'charged - '. $class->credited_sql($start, $end);
5586 Returns an SQL fragment to retreive the amount paid against this invoice.
5591 my ($class, $start, $end) = @_;
5592 $start &&= "AND cust_bill_pay._date <= $start";
5593 $end &&= "AND cust_bill_pay._date > $end";
5594 $start = '' unless defined($start);
5595 $end = '' unless defined($end);
5596 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5597 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5602 Returns an SQL fragment to retreive the amount credited against this invoice.
5607 my ($class, $start, $end) = @_;
5608 $start &&= "AND cust_credit_bill._date <= $start";
5609 $end &&= "AND cust_credit_bill._date > $end";
5610 $start = '' unless defined($start);
5611 $end = '' unless defined($end);
5612 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5613 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5618 Returns an SQL fragment to retrieve the due date of an invoice.
5619 Currently only supported on PostgreSQL.
5624 my $conf = new FS::Conf;
5628 cust_bill.invoice_terms,
5629 cust_main.invoice_terms,
5630 \''.($conf->config('invoice_default_terms') || '').'\'
5631 ), E\'Net (\\\\d+)\'
5633 ) * 86400 + cust_bill._date'
5636 =item search_sql_where HASHREF
5638 Class method which returns an SQL WHERE fragment to search for parameters
5639 specified in HASHREF. Valid parameters are
5645 List reference of start date, end date, as UNIX timestamps.
5655 List reference of charged limits (exclusive).
5659 List reference of charged limits (exclusive).
5663 flag, return open invoices only
5667 flag, return net invoices only
5671 =item newest_percust
5675 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5679 sub search_sql_where {
5680 my($class, $param) = @_;
5682 warn "$me search_sql_where called with params: \n".
5683 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5689 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5690 push @search, "cust_main.agentnum = $1";
5694 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5695 push @search, "cust_main.refnum = $1";
5699 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5700 push @search, "cust_bill.custnum = $1";
5704 if ( $param->{'cust_classnum'} ) {
5705 my $classnums = $param->{'cust_classnum'};
5706 $classnums = [ $classnums ] if !ref($classnums);
5707 $classnums = [ grep /^\d+$/, @$classnums ];
5708 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5713 if ( $param->{_date} ) {
5714 my($beginning, $ending) = @{$param->{_date}};
5716 push @search, "cust_bill._date >= $beginning",
5717 "cust_bill._date < $ending";
5721 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5722 push @search, "cust_bill.invnum >= $1";
5724 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5725 push @search, "cust_bill.invnum <= $1";
5729 if ( $param->{charged} ) {
5730 my @charged = ref($param->{charged})
5731 ? @{ $param->{charged} }
5732 : ($param->{charged});
5734 push @search, map { s/^charged/cust_bill.charged/; $_; }
5738 my $owed_sql = FS::cust_bill->owed_sql;
5741 if ( $param->{owed} ) {
5742 my @owed = ref($param->{owed})
5743 ? @{ $param->{owed} }
5745 push @search, map { s/^owed/$owed_sql/; $_; }
5750 push @search, "0 != $owed_sql"
5751 if $param->{'open'};
5752 push @search, '0 != '. FS::cust_bill->net_sql
5756 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5757 if $param->{'days'};
5760 if ( $param->{'newest_percust'} ) {
5762 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5763 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5765 my @newest_where = map { my $x = $_;
5766 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5769 grep ! /^cust_main./, @search;
5770 my $newest_where = scalar(@newest_where)
5771 ? ' AND '. join(' AND ', @newest_where)
5775 push @search, "cust_bill._date = (
5776 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5777 WHERE newest_cust_bill.custnum = cust_bill.custnum
5783 #promised_date - also has an option to accept nulls
5784 if ( $param->{promised_date} ) {
5785 my($beginning, $ending, $null) = @{$param->{promised_date}};
5787 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5788 "cust_bill.promised_date < $ending )" .
5789 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5792 #agent virtualization
5793 my $curuser = $FS::CurrentUser::CurrentUser;
5794 if ( $curuser->username eq 'fs_queue'
5795 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5797 my $newuser = qsearchs('access_user', {
5798 'username' => $username,
5802 $curuser = $newuser;
5804 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5807 push @search, $curuser->agentnums_sql;
5809 join(' AND ', @search );
5821 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5822 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base