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;
48 @ISA = qw( FS::cust_main_Mixin FS::Record );
51 $me = '[FS::cust_bill]';
53 #ask FS::UID to run this stuff for us later
54 FS::UID->install_callback( sub {
55 my $conf = new FS::Conf; #global
56 $money_char = $conf->config('money_char') || '$';
57 $date_format = $conf->config('date_format') || '%x'; #/YY
58 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
59 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
64 FS::cust_bill - Object methods for cust_bill records
70 $record = new FS::cust_bill \%hash;
71 $record = new FS::cust_bill { 'column' => 'value' };
73 $error = $record->insert;
75 $error = $new_record->replace($old_record);
77 $error = $record->delete;
79 $error = $record->check;
81 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
83 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
85 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
87 @cust_pay_objects = $cust_bill->cust_pay;
89 $tax_amount = $record->tax;
91 @lines = $cust_bill->print_text;
92 @lines = $cust_bill->print_text $time;
96 An FS::cust_bill object represents an invoice; a declaration that a customer
97 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
98 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
99 following fields are currently supported:
105 =item invnum - primary key (assigned automatically for new invoices)
107 =item custnum - customer (see L<FS::cust_main>)
109 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
110 L<Time::Local> and L<Date::Parse> for conversion functions.
112 =item charged - amount of this invoice
114 =item invoice_terms - optional terms override for this specific invoice
118 Customer info at invoice generation time
122 =item previous_balance
124 =item billing_balance
132 =item printed - deprecated
140 =item closed - books closed flag, empty or `Y'
142 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
144 =item agent_invid - legacy invoice number
154 Creates a new invoice. To add the invoice to the database, see L<"insert">.
155 Invoices are normally created by calling the bill method of a customer object
156 (see L<FS::cust_main>).
160 sub table { 'cust_bill'; }
162 sub cust_linked { $_[0]->cust_main_custnum; }
163 sub cust_unlinked_msg {
165 "WARNING: can't find cust_main.custnum ". $self->custnum.
166 ' (cust_bill.invnum '. $self->invnum. ')';
171 Adds this invoice to the database ("Posts" the invoice). If there is an error,
172 returns the error, otherwise returns false.
178 warn "$me insert called\n" if $DEBUG;
180 local $SIG{HUP} = 'IGNORE';
181 local $SIG{INT} = 'IGNORE';
182 local $SIG{QUIT} = 'IGNORE';
183 local $SIG{TERM} = 'IGNORE';
184 local $SIG{TSTP} = 'IGNORE';
185 local $SIG{PIPE} = 'IGNORE';
187 my $oldAutoCommit = $FS::UID::AutoCommit;
188 local $FS::UID::AutoCommit = 0;
191 my $error = $self->SUPER::insert;
193 $dbh->rollback if $oldAutoCommit;
197 if ( $self->get('cust_bill_pkg') ) {
198 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
199 $cust_bill_pkg->invnum($self->invnum);
200 my $error = $cust_bill_pkg->insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "can't create invoice line item: $error";
208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
215 This method now works but you probably shouldn't use it. Instead, apply a
216 credit against the invoice.
218 Using this method to delete invoices outright is really, really bad. There
219 would be no record you ever posted this invoice, and there are no check to
220 make sure charged = 0 or that there are no associated cust_bill_pkg records.
222 Really, don't use it.
228 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
230 local $SIG{HUP} = 'IGNORE';
231 local $SIG{INT} = 'IGNORE';
232 local $SIG{QUIT} = 'IGNORE';
233 local $SIG{TERM} = 'IGNORE';
234 local $SIG{TSTP} = 'IGNORE';
235 local $SIG{PIPE} = 'IGNORE';
237 my $oldAutoCommit = $FS::UID::AutoCommit;
238 local $FS::UID::AutoCommit = 0;
241 foreach my $table (qw(
253 foreach my $linked ( $self->$table() ) {
254 my $error = $linked->delete;
256 $dbh->rollback if $oldAutoCommit;
263 my $error = $self->SUPER::delete(@_);
265 $dbh->rollback if $oldAutoCommit;
269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275 =item replace [ OLD_RECORD ]
277 You can, but probably shouldn't modify invoices...
279 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
280 supplied, replaces this record. If there is an error, returns the error,
281 otherwise returns false.
285 #replace can be inherited from Record.pm
287 # replace_check is now the preferred way to #implement replace data checks
288 # (so $object->replace() works without an argument)
291 my( $new, $old ) = ( shift, shift );
292 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
293 #return "Can't change _date!" unless $old->_date eq $new->_date;
294 return "Can't change _date" unless $old->_date == $new->_date;
295 return "Can't change charged" unless $old->charged == $new->charged
296 || $old->charged == 0
297 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
303 =item add_cc_surcharge
309 sub add_cc_surcharge {
310 my ($self, $pkgnum, $amount) = (shift, shift, shift);
313 my $cust_bill_pkg = new FS::cust_bill_pkg({
314 'invnum' => $self->invnum,
318 $error = $cust_bill_pkg->insert;
319 return $error if $error;
321 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
322 $self->charged($self->charged+$amount);
323 $error = $self->replace;
324 return $error if $error;
326 $self->apply_payments_and_credits;
332 Checks all fields to make sure this is a valid invoice. If there is an error,
333 returns the error, otherwise returns false. Called by the insert and replace
342 $self->ut_numbern('invnum')
343 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
344 || $self->ut_numbern('_date')
345 || $self->ut_money('charged')
346 || $self->ut_numbern('printed')
347 || $self->ut_enum('closed', [ '', 'Y' ])
348 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
349 || $self->ut_numbern('agent_invid') #varchar?
351 return $error if $error;
353 $self->_date(time) unless $self->_date;
355 $self->printed(0) if $self->printed eq '';
362 Returns the displayed invoice number for this invoice: agent_invid if
363 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
369 my $conf = $self->conf;
370 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
371 return $self->agent_invid;
373 return $self->invnum;
379 Returns a list consisting of the total previous balance for this customer,
380 followed by the previous outstanding invoices (as FS::cust_bill objects also).
387 my @cust_bill = sort { $a->_date <=> $b->_date }
388 grep { $_->owed != 0 && $_->_date < $self->_date }
389 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
391 foreach ( @cust_bill ) { $total += $_->owed; }
397 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
404 { 'table' => 'cust_bill_pkg',
405 'hashref' => { 'invnum' => $self->invnum },
406 'order_by' => 'ORDER BY billpkgnum',
411 =item cust_bill_pkg_pkgnum PKGNUM
413 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
418 sub cust_bill_pkg_pkgnum {
419 my( $self, $pkgnum ) = @_;
421 { 'table' => 'cust_bill_pkg',
422 'hashref' => { 'invnum' => $self->invnum,
425 'order_by' => 'ORDER BY billpkgnum',
432 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
439 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
440 $self->cust_bill_pkg;
442 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
447 Returns true if any of the packages (or their definitions) corresponding to the
448 line items for this invoice have the no_auto flag set.
454 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
457 =item open_cust_bill_pkg
459 Returns the open line items for this invoice.
461 Note that cust_bill_pkg with both setup and recur fees are returned as two
462 separate line items, each with only one fee.
466 # modeled after cust_main::open_cust_bill
467 sub open_cust_bill_pkg {
470 # grep { $_->owed > 0 } $self->cust_bill_pkg
472 my %other = ( 'recur' => 'setup',
473 'setup' => 'recur', );
475 foreach my $field ( qw( recur setup )) {
476 push @open, map { $_->set( $other{$field}, 0 ); $_; }
477 grep { $_->owed($field) > 0 }
478 $self->cust_bill_pkg;
484 =item cust_bill_event
486 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
490 sub cust_bill_event {
492 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
495 =item num_cust_bill_event
497 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
501 sub num_cust_bill_event {
504 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
505 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
506 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
507 $sth->fetchrow_arrayref->[0];
512 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
516 #false laziness w/cust_pkg.pm
520 'table' => 'cust_event',
521 'addl_from' => 'JOIN part_event USING ( eventpart )',
522 'hashref' => { 'tablenum' => $self->invnum },
523 'extra_sql' => " AND eventtable = 'cust_bill' ",
529 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
533 #false laziness w/cust_pkg.pm
537 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
538 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
539 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
540 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
541 $sth->fetchrow_arrayref->[0];
546 Returns the customer (see L<FS::cust_main>) for this invoice.
552 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
555 =item cust_suspend_if_balance_over AMOUNT
557 Suspends the customer associated with this invoice if the total amount owed on
558 this invoice and all older invoices is greater than the specified amount.
560 Returns a list: an empty list on success or a list of errors.
564 sub cust_suspend_if_balance_over {
565 my( $self, $amount ) = ( shift, shift );
566 my $cust_main = $self->cust_main;
567 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
570 $cust_main->suspend(@_);
576 Depreciated. See the cust_credited method.
578 #Returns a list consisting of the total previous credited (see
579 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
580 #outstanding credits (FS::cust_credit objects).
586 croak "FS::cust_bill->cust_credit depreciated; see ".
587 "FS::cust_bill->cust_credit_bill";
590 #my @cust_credit = sort { $a->_date <=> $b->_date }
591 # grep { $_->credited != 0 && $_->_date < $self->_date }
592 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
594 #foreach (@cust_credit) { $total += $_->credited; }
595 #$total, @cust_credit;
600 Depreciated. See the cust_bill_pay method.
602 #Returns all payments (see L<FS::cust_pay>) for this invoice.
608 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
610 #sort { $a->_date <=> $b->_date }
611 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
617 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
620 sub cust_bill_pay_batch {
622 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
627 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
633 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
634 sort { $a->_date <=> $b->_date }
635 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
640 =item cust_credit_bill
642 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
648 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
649 sort { $a->_date <=> $b->_date }
650 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
654 sub cust_credit_bill {
655 shift->cust_credited(@_);
658 #=item cust_bill_pay_pkgnum PKGNUM
660 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
661 #with matching pkgnum.
665 #sub cust_bill_pay_pkgnum {
666 # my( $self, $pkgnum ) = @_;
667 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
668 # sort { $a->_date <=> $b->_date }
669 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
670 # 'pkgnum' => $pkgnum,
675 =item cust_bill_pay_pkg PKGNUM
677 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
678 applied against the matching pkgnum.
682 sub cust_bill_pay_pkg {
683 my( $self, $pkgnum ) = @_;
686 'select' => 'cust_bill_pay_pkg.*',
687 'table' => 'cust_bill_pay_pkg',
688 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
689 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
690 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
691 " AND cust_bill_pkg.pkgnum = $pkgnum",
696 #=item cust_credited_pkgnum PKGNUM
698 #=item cust_credit_bill_pkgnum PKGNUM
700 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
701 #with matching pkgnum.
705 #sub cust_credited_pkgnum {
706 # my( $self, $pkgnum ) = @_;
707 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
708 # sort { $a->_date <=> $b->_date }
709 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
710 # 'pkgnum' => $pkgnum,
715 #sub cust_credit_bill_pkgnum {
716 # shift->cust_credited_pkgnum(@_);
719 =item cust_credit_bill_pkg PKGNUM
721 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
722 applied against the matching pkgnum.
726 sub cust_credit_bill_pkg {
727 my( $self, $pkgnum ) = @_;
730 'select' => 'cust_credit_bill_pkg.*',
731 'table' => 'cust_credit_bill_pkg',
732 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
733 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
734 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
735 " AND cust_bill_pkg.pkgnum = $pkgnum",
740 =item cust_bill_batch
742 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
746 sub cust_bill_batch {
748 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
753 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
760 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
762 foreach (@taxlines) { $total += $_->setup; }
768 Returns the amount owed (still outstanding) on this invoice, which is charged
769 minus all payment applications (see L<FS::cust_bill_pay>) and credit
770 applications (see L<FS::cust_credit_bill>).
776 my $balance = $self->charged;
777 $balance -= $_->amount foreach ( $self->cust_bill_pay );
778 $balance -= $_->amount foreach ( $self->cust_credited );
779 $balance = sprintf( "%.2f", $balance);
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
785 my( $self, $pkgnum ) = @_;
787 #my $balance = $self->charged;
789 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
791 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
792 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
794 $balance = sprintf( "%.2f", $balance);
795 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
799 =item apply_payments_and_credits [ OPTION => VALUE ... ]
801 Applies unapplied payments and credits to this invoice.
803 A hash of optional arguments may be passed. Currently "manual" is supported.
804 If true, a payment receipt is sent instead of a statement when
805 'payment_receipt_email' configuration option is set.
807 If there is an error, returns the error, otherwise returns false.
811 sub apply_payments_and_credits {
812 my( $self, %options ) = @_;
813 my $conf = $self->conf;
815 local $SIG{HUP} = 'IGNORE';
816 local $SIG{INT} = 'IGNORE';
817 local $SIG{QUIT} = 'IGNORE';
818 local $SIG{TERM} = 'IGNORE';
819 local $SIG{TSTP} = 'IGNORE';
820 local $SIG{PIPE} = 'IGNORE';
822 my $oldAutoCommit = $FS::UID::AutoCommit;
823 local $FS::UID::AutoCommit = 0;
826 $self->select_for_update; #mutex
828 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
829 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
831 if ( $conf->exists('pkg-balances') ) {
832 # limit @payments & @credits to those w/ a pkgnum grepped from $self
833 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
834 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
835 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
838 while ( $self->owed > 0 and ( @payments || @credits ) ) {
841 if ( @payments && @credits ) {
843 #decide which goes first by weight of top (unapplied) line item
845 my @open_lineitems = $self->open_cust_bill_pkg;
848 max( map { $_->part_pkg->pay_weight || 0 }
853 my $max_credit_weight =
854 max( map { $_->part_pkg->credit_weight || 0 }
860 #if both are the same... payments first? it has to be something
861 if ( $max_pay_weight >= $max_credit_weight ) {
867 } elsif ( @payments ) {
869 } elsif ( @credits ) {
872 die "guru meditation #12 and 35";
876 if ( $app eq 'pay' ) {
878 my $payment = shift @payments;
879 $unapp_amount = $payment->unapplied;
880 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
881 $app->pkgnum( $payment->pkgnum )
882 if $conf->exists('pkg-balances') && $payment->pkgnum;
884 } elsif ( $app eq 'credit' ) {
886 my $credit = shift @credits;
887 $unapp_amount = $credit->credited;
888 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
889 $app->pkgnum( $credit->pkgnum )
890 if $conf->exists('pkg-balances') && $credit->pkgnum;
893 die "guru meditation #12 and 35";
897 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
898 warn "owed_pkgnum ". $app->pkgnum;
899 $owed = $self->owed_pkgnum($app->pkgnum);
903 next unless $owed > 0;
905 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
906 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
908 $app->invnum( $self->invnum );
910 my $error = $app->insert(%options);
912 $dbh->rollback if $oldAutoCommit;
913 return "Error inserting ". $app->table. " record: $error";
915 die $error if $error;
919 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
924 =item generate_email OPTION => VALUE ...
932 sender address, required
936 alternate template name, optional
940 text attachment arrayref, optional
944 email subject, optional
948 notice name instead of "Invoice", optional
952 Returns an argument list to be passed to L<FS::Misc::send_email>.
962 my $conf = $self->conf;
964 my $me = '[FS::cust_bill::generate_email]';
967 'from' => $args{'from'},
968 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
972 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
973 'template' => $args{'template'},
974 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
975 'no_coupon' => $args{'no_coupon'},
978 my $cust_main = $self->cust_main;
980 if (ref($args{'to'}) eq 'ARRAY') {
981 $return{'to'} = $args{'to'};
983 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
984 $cust_main->invoicing_list
988 if ( $conf->exists('invoice_html') ) {
990 warn "$me creating HTML/text multipart message"
993 $return{'nobody'} = 1;
995 my $alternative = build MIME::Entity
996 'Type' => 'multipart/alternative',
997 #'Encoding' => '7bit',
998 'Disposition' => 'inline'
1002 if ( $conf->exists('invoice_email_pdf')
1003 and scalar($conf->config('invoice_email_pdf_note')) ) {
1005 warn "$me using 'invoice_email_pdf_note' in multipart message"
1007 $data = [ map { $_ . "\n" }
1008 $conf->config('invoice_email_pdf_note')
1013 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1015 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1016 $data = $args{'print_text'};
1018 $data = [ $self->print_text(\%opt) ];
1023 $alternative->attach(
1024 'Type' => 'text/plain',
1025 'Encoding' => 'quoted-printable',
1026 #'Encoding' => '7bit',
1028 'Disposition' => 'inline',
1031 $args{'from'} =~ /\@([\w\.\-]+)/;
1032 my $from = $1 || 'example.com';
1033 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1036 my $agentnum = $cust_main->agentnum;
1037 if ( defined($args{'template'}) && length($args{'template'})
1038 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1041 $logo = 'logo_'. $args{'template'}. '.png';
1045 my $image_data = $conf->config_binary( $logo, $agentnum);
1047 my $image = build MIME::Entity
1048 'Type' => 'image/png',
1049 'Encoding' => 'base64',
1050 'Data' => $image_data,
1051 'Filename' => 'logo.png',
1052 'Content-ID' => "<$content_id>",
1056 if($conf->exists('invoice-barcode')){
1057 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1058 $barcode = build MIME::Entity
1059 'Type' => 'image/png',
1060 'Encoding' => 'base64',
1061 'Data' => $self->invoice_barcode(0),
1062 'Filename' => 'barcode.png',
1063 'Content-ID' => "<$barcode_content_id>",
1065 $opt{'barcode_cid'} = $barcode_content_id;
1068 $alternative->attach(
1069 'Type' => 'text/html',
1070 'Encoding' => 'quoted-printable',
1071 'Data' => [ '<html>',
1074 ' '. encode_entities($return{'subject'}),
1077 ' <body bgcolor="#e8e8e8">',
1078 $self->print_html({ 'cid'=>$content_id, %opt }),
1082 'Disposition' => 'inline',
1083 #'Filename' => 'invoice.pdf',
1086 my @otherparts = ();
1087 if ( $cust_main->email_csv_cdr ) {
1089 push @otherparts, build MIME::Entity
1090 'Type' => 'text/csv',
1091 'Encoding' => '7bit',
1092 'Data' => [ map { "$_\n" }
1093 $self->call_details('prepend_billed_number' => 1)
1095 'Disposition' => 'attachment',
1096 'Filename' => 'usage-'. $self->invnum. '.csv',
1101 if ( $conf->exists('invoice_email_pdf') ) {
1106 # multipart/alternative
1112 my $related = build MIME::Entity 'Type' => 'multipart/related',
1113 'Encoding' => '7bit';
1115 #false laziness w/Misc::send_email
1116 $related->head->replace('Content-type',
1117 $related->mime_type.
1118 '; boundary="'. $related->head->multipart_boundary. '"'.
1119 '; type=multipart/alternative'
1122 $related->add_part($alternative);
1124 $related->add_part($image);
1126 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1128 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1132 #no other attachment:
1134 # multipart/alternative
1139 $return{'content-type'} = 'multipart/related';
1140 if($conf->exists('invoice-barcode')){
1141 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1144 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1146 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1147 #$return{'disposition'} = 'inline';
1153 if ( $conf->exists('invoice_email_pdf') ) {
1154 warn "$me creating PDF attachment"
1157 #mime parts arguments a la MIME::Entity->build().
1158 $return{'mimeparts'} = [
1159 { $self->mimebuild_pdf(\%opt) }
1163 if ( $conf->exists('invoice_email_pdf')
1164 and scalar($conf->config('invoice_email_pdf_note')) ) {
1166 warn "$me using 'invoice_email_pdf_note'"
1168 $return{'body'} = [ map { $_ . "\n" }
1169 $conf->config('invoice_email_pdf_note')
1174 warn "$me not using 'invoice_email_pdf_note'"
1176 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1177 $return{'body'} = $args{'print_text'};
1179 $return{'body'} = [ $self->print_text(\%opt) ];
1192 Returns a list suitable for passing to MIME::Entity->build(), representing
1193 this invoice as PDF attachment.
1200 'Type' => 'application/pdf',
1201 'Encoding' => 'base64',
1202 'Data' => [ $self->print_pdf(@_) ],
1203 'Disposition' => 'attachment',
1204 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1208 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1210 Sends this invoice to the destinations configured for this customer: sends
1211 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1213 Options can be passed as a hashref (recommended) or as a list of up to
1214 four values for templatename, agentnum, invoice_from and amount.
1216 I<template>, if specified, is the name of a suffix for alternate invoices.
1218 I<agentnum>, if specified, means that this invoice will only be sent for customers
1219 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1220 single agent) or an arrayref of agentnums.
1222 I<invoice_from>, if specified, overrides the default email invoice From: address.
1224 I<amount>, if specified, only sends the invoice if the total amount owed on this
1225 invoice and all older invoices is greater than the specified amount.
1227 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1231 sub queueable_send {
1234 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1235 or die "invalid invoice number: " . $opt{invnum};
1237 my @args = ( $opt{template}, $opt{agentnum} );
1238 push @args, $opt{invoice_from}
1239 if exists($opt{invoice_from}) && $opt{invoice_from};
1241 my $error = $self->send( @args );
1242 die $error if $error;
1248 my $conf = $self->conf;
1250 my( $template, $invoice_from, $notice_name );
1252 my $balance_over = 0;
1256 $template = $opt->{'template'} || '';
1257 if ( $agentnums = $opt->{'agentnum'} ) {
1258 $agentnums = [ $agentnums ] unless ref($agentnums);
1260 $invoice_from = $opt->{'invoice_from'};
1261 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1262 $notice_name = $opt->{'notice_name'};
1264 $template = scalar(@_) ? shift : '';
1265 if ( scalar(@_) && $_[0] ) {
1266 $agentnums = ref($_[0]) ? shift : [ shift ];
1268 $invoice_from = shift if scalar(@_);
1269 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1272 return 'N/A' unless ! $agentnums
1273 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1276 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1278 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1279 $conf->config('invoice_from', $self->cust_main->agentnum );
1282 'template' => $template,
1283 'invoice_from' => $invoice_from,
1284 'notice_name' => ( $notice_name || 'Invoice' ),
1287 my @invoicing_list = $self->cust_main->invoicing_list;
1289 #$self->email_invoice(\%opt)
1291 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1293 #$self->print_invoice(\%opt)
1295 if grep { $_ eq 'POST' } @invoicing_list; #postal
1297 $self->fax_invoice(\%opt)
1298 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1304 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1306 Emails this invoice.
1308 Options can be passed as a hashref (recommended) or as a list of up to
1309 two values for templatename and invoice_from.
1311 I<template>, if specified, is the name of a suffix for alternate invoices.
1313 I<invoice_from>, if specified, overrides the default email invoice From: address.
1315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1319 sub queueable_email {
1322 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1323 or die "invalid invoice number: " . $opt{invnum};
1325 my %args = ( 'template' => $opt{template} );
1326 $args{$_} = $opt{$_}
1327 foreach grep { exists($opt{$_}) && $opt{$_} }
1328 qw( invoice_from notice_name no_coupon );
1330 my $error = $self->email( \%args );
1331 die $error if $error;
1335 #sub email_invoice {
1338 my $conf = $self->conf;
1340 my( $template, $invoice_from, $notice_name, $no_coupon );
1343 $template = $opt->{'template'} || '';
1344 $invoice_from = $opt->{'invoice_from'};
1345 $notice_name = $opt->{'notice_name'} || 'Invoice';
1346 $no_coupon = $opt->{'no_coupon'} || 0;
1348 $template = scalar(@_) ? shift : '';
1349 $invoice_from = shift if scalar(@_);
1350 $notice_name = 'Invoice';
1354 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1355 $conf->config('invoice_from', $self->cust_main->agentnum );
1357 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1358 $self->cust_main->invoicing_list;
1360 if ( ! @invoicing_list ) { #no recipients
1361 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1362 die 'No recipients for customer #'. $self->custnum;
1364 #default: better to notify this person than silence
1365 @invoicing_list = ($invoice_from);
1369 my $subject = $self->email_subject($template);
1371 my $error = send_email(
1372 $self->generate_email(
1373 'from' => $invoice_from,
1374 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1375 'subject' => $subject,
1376 'template' => $template,
1377 'notice_name' => $notice_name,
1378 'no_coupon' => $no_coupon,
1381 die "can't email invoice: $error\n" if $error;
1382 #die "$error\n" if $error;
1388 my $conf = $self->conf;
1390 #my $template = scalar(@_) ? shift : '';
1393 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1396 my $cust_main = $self->cust_main;
1397 my $name = $cust_main->name;
1398 my $name_short = $cust_main->name_short;
1399 my $invoice_number = $self->invnum;
1400 my $invoice_date = $self->_date_pretty;
1402 eval qq("$subject");
1405 =item lpr_data HASHREF | [ TEMPLATE ]
1407 Returns the postscript or plaintext for this invoice as an arrayref.
1409 Options can be passed as a hashref (recommended) or as a single optional value
1412 I<template>, if specified, is the name of a suffix for alternate invoices.
1414 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1420 my $conf = $self->conf;
1421 my( $template, $notice_name );
1424 $template = $opt->{'template'} || '';
1425 $notice_name = $opt->{'notice_name'} || 'Invoice';
1427 $template = scalar(@_) ? shift : '';
1428 $notice_name = 'Invoice';
1432 'template' => $template,
1433 'notice_name' => $notice_name,
1436 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1437 [ $self->$method( \%opt ) ];
1440 =item print HASHREF | [ TEMPLATE ]
1442 Prints this invoice.
1444 Options can be passed as a hashref (recommended) or as a single optional
1447 I<template>, if specified, is the name of a suffix for alternate invoices.
1449 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1453 #sub print_invoice {
1456 my $conf = $self->conf;
1457 my( $template, $notice_name );
1460 $template = $opt->{'template'} || '';
1461 $notice_name = $opt->{'notice_name'} || 'Invoice';
1463 $template = scalar(@_) ? shift : '';
1464 $notice_name = 'Invoice';
1468 'template' => $template,
1469 'notice_name' => $notice_name,
1472 if($conf->exists('invoice_print_pdf')) {
1473 # Add the invoice to the current batch.
1474 $self->batch_invoice(\%opt);
1477 do_print $self->lpr_data(\%opt);
1481 =item fax_invoice HASHREF | [ TEMPLATE ]
1485 Options can be passed as a hashref (recommended) or as a single optional
1488 I<template>, if specified, is the name of a suffix for alternate invoices.
1490 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1496 my $conf = $self->conf;
1497 my( $template, $notice_name );
1500 $template = $opt->{'template'} || '';
1501 $notice_name = $opt->{'notice_name'} || 'Invoice';
1503 $template = scalar(@_) ? shift : '';
1504 $notice_name = 'Invoice';
1507 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1508 unless $conf->exists('invoice_latex');
1510 my $dialstring = $self->cust_main->getfield('fax');
1514 'template' => $template,
1515 'notice_name' => $notice_name,
1518 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1519 'dialstring' => $dialstring,
1521 die $error if $error;
1525 =item batch_invoice [ HASHREF ]
1527 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1528 isn't an open batch, one will be created.
1533 my ($self, $opt) = @_;
1534 my $bill_batch = $self->get_open_bill_batch;
1535 my $cust_bill_batch = FS::cust_bill_batch->new({
1536 batchnum => $bill_batch->batchnum,
1537 invnum => $self->invnum,
1539 return $cust_bill_batch->insert($opt);
1542 =item get_open_batch
1544 Returns the currently open batch as an FS::bill_batch object, creating a new
1545 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1550 sub get_open_bill_batch {
1552 my $conf = $self->conf;
1553 my $hashref = { status => 'O' };
1554 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1555 ? $self->cust_main->agentnum
1557 my $batch = qsearchs('bill_batch', $hashref);
1558 return $batch if $batch;
1559 $batch = FS::bill_batch->new($hashref);
1560 my $error = $batch->insert;
1561 die $error if $error;
1565 =item ftp_invoice [ TEMPLATENAME ]
1567 Sends this invoice data via FTP.
1569 TEMPLATENAME is unused?
1575 my $conf = $self->conf;
1576 my $template = scalar(@_) ? shift : '';
1579 'protocol' => 'ftp',
1580 'server' => $conf->config('cust_bill-ftpserver'),
1581 'username' => $conf->config('cust_bill-ftpusername'),
1582 'password' => $conf->config('cust_bill-ftppassword'),
1583 'dir' => $conf->config('cust_bill-ftpdir'),
1584 'format' => $conf->config('cust_bill-ftpformat'),
1588 =item spool_invoice [ TEMPLATENAME ]
1590 Spools this invoice data (see L<FS::spool_csv>)
1592 TEMPLATENAME is unused?
1598 my $conf = $self->conf;
1599 my $template = scalar(@_) ? shift : '';
1602 'format' => $conf->config('cust_bill-spoolformat'),
1603 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1607 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1609 Like B<send>, but only sends the invoice if it is the newest open invoice for
1614 sub send_if_newest {
1619 grep { $_->owed > 0 }
1620 qsearch('cust_bill', {
1621 'custnum' => $self->custnum,
1622 #'_date' => { op=>'>', value=>$self->_date },
1623 'invnum' => { op=>'>', value=>$self->invnum },
1630 =item send_csv OPTION => VALUE, ...
1632 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1636 protocol - currently only "ftp"
1642 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1643 and YYMMDDHHMMSS is a timestamp.
1645 See L</print_csv> for a description of the output format.
1650 my($self, %opt) = @_;
1654 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1655 mkdir $spooldir, 0700 unless -d $spooldir;
1657 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1658 my $file = "$spooldir/$tracctnum.csv";
1660 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1662 open(CSV, ">$file") or die "can't open $file: $!";
1670 if ( $opt{protocol} eq 'ftp' ) {
1671 eval "use Net::FTP;";
1673 $net = Net::FTP->new($opt{server}) or die @$;
1675 die "unknown protocol: $opt{protocol}";
1678 $net->login( $opt{username}, $opt{password} )
1679 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1681 $net->binary or die "can't set binary mode";
1683 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1685 $net->put($file) or die "can't put $file: $!";
1695 Spools CSV invoice data.
1701 =item format - 'default' or 'billco'
1703 =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>).
1705 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1707 =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.
1714 my($self, %opt) = @_;
1716 my $cust_main = $self->cust_main;
1718 if ( $opt{'dest'} ) {
1719 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1720 $cust_main->invoicing_list;
1721 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1722 || ! keys %invoicing_list;
1725 if ( $opt{'balanceover'} ) {
1727 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1730 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1731 mkdir $spooldir, 0700 unless -d $spooldir;
1733 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1737 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1738 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1741 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1743 open(CSV, ">>$file") or die "can't open $file: $!";
1744 flock(CSV, LOCK_EX);
1749 if ( lc($opt{'format'}) eq 'billco' ) {
1751 flock(CSV, LOCK_UN);
1756 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1759 open(CSV,">>$file") or die "can't open $file: $!";
1760 flock(CSV, LOCK_EX);
1766 flock(CSV, LOCK_UN);
1773 =item print_csv OPTION => VALUE, ...
1775 Returns CSV data for this invoice.
1779 format - 'default' or 'billco'
1781 Returns a list consisting of two scalars. The first is a single line of CSV
1782 header information for this invoice. The second is one or more lines of CSV
1783 detail information for this invoice.
1785 If I<format> is not specified or "default", the fields of the CSV file are as
1788 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1792 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1794 B<record_type> is C<cust_bill> for the initial header line only. The
1795 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1796 fields are filled in.
1798 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1799 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1802 =item invnum - invoice number
1804 =item custnum - customer number
1806 =item _date - invoice date
1808 =item charged - total invoice amount
1810 =item first - customer first name
1812 =item last - customer first name
1814 =item company - company name
1816 =item address1 - address line 1
1818 =item address2 - address line 1
1828 =item pkg - line item description
1830 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1832 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1834 =item sdate - start date for recurring fee
1836 =item edate - end date for recurring fee
1840 If I<format> is "billco", the fields of the header CSV file are as follows:
1842 +-------------------------------------------------------------------+
1843 | FORMAT HEADER FILE |
1844 |-------------------------------------------------------------------|
1845 | Field | Description | Name | Type | Width |
1846 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1847 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1848 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1849 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1850 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1851 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1852 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1853 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1854 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1855 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1856 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1857 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1858 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1859 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1860 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1861 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1862 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1863 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1864 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1865 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1866 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1867 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1868 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1869 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1870 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1871 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1872 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1873 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1874 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1875 +-------+-------------------------------+------------+------+-------+
1877 If I<format> is "billco", the fields of the detail CSV file are as follows:
1879 FORMAT FOR DETAIL FILE
1881 Field | Description | Name | Type | Width
1882 1 | N/A-Leave Empty | RC | CHAR | 2
1883 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1884 3 | Account Number | TRACCTNUM | CHAR | 15
1885 4 | Invoice Number | TRINVOICE | CHAR | 15
1886 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1887 6 | Transaction Detail | DETAILS | CHAR | 100
1888 7 | Amount | AMT | NUM* | 9
1889 8 | Line Format Control** | LNCTRL | CHAR | 2
1890 9 | Grouping Code | GROUP | CHAR | 2
1891 10 | User Defined | ACCT CODE | CHAR | 15
1896 my($self, %opt) = @_;
1898 eval "use Text::CSV_XS";
1901 my $cust_main = $self->cust_main;
1903 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1905 if ( lc($opt{'format'}) eq 'billco' ) {
1908 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1910 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1912 my( $previous_balance, @unused ) = $self->previous; #previous balance
1914 my $pmt_cr_applied = 0;
1915 $pmt_cr_applied += $_->{'amount'}
1916 foreach ( $self->_items_payments, $self->_items_credits ) ;
1918 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1921 '', # 1 | N/A-Leave Empty CHAR 2
1922 '', # 2 | N/A-Leave Empty CHAR 15
1923 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1924 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1925 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1926 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1927 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1928 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1929 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1930 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1931 '', # 10 | Ancillary Billing Information CHAR 30
1932 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1933 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1936 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1939 $duedate, # 14 | Bill Due Date CHAR 10
1941 $previous_balance, # 15 | Previous Balance NUM* 9
1942 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1943 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1944 $totaldue, # 18 | Total Amt Due NUM* 9
1945 $totaldue, # 19 | Total Amt Due NUM* 9
1946 '', # 20 | 30 Day Aging NUM* 9
1947 '', # 21 | 60 Day Aging NUM* 9
1948 '', # 22 | 90 Day Aging NUM* 9
1949 'N', # 23 | Y/N CHAR 1
1950 '', # 24 | Remittance automation CHAR 100
1951 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1952 $self->custnum, # 26 | Customer Reference Number CHAR 15
1953 '0', # 27 | Federal Tax*** NUM* 9
1954 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1955 '0', # 29 | Other Taxes & Fees*** NUM* 9
1964 time2str("%x", $self->_date),
1965 sprintf("%.2f", $self->charged),
1966 ( map { $cust_main->getfield($_) }
1967 qw( first last company address1 address2 city state zip country ) ),
1969 ) or die "can't create csv";
1972 my $header = $csv->string. "\n";
1975 if ( lc($opt{'format'}) eq 'billco' ) {
1978 foreach my $item ( $self->_items_pkg ) {
1981 '', # 1 | N/A-Leave Empty CHAR 2
1982 '', # 2 | N/A-Leave Empty CHAR 15
1983 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1984 $self->invnum, # 4 | Invoice Number CHAR 15
1985 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1986 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1987 $item->{'amount'}, # 7 | Amount NUM* 9
1988 '', # 8 | Line Format Control** CHAR 2
1989 '', # 9 | Grouping Code CHAR 2
1990 '', # 10 | User Defined CHAR 15
1993 $detail .= $csv->string. "\n";
1999 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2001 my($pkg, $setup, $recur, $sdate, $edate);
2002 if ( $cust_bill_pkg->pkgnum ) {
2004 ($pkg, $setup, $recur, $sdate, $edate) = (
2005 $cust_bill_pkg->part_pkg->pkg,
2006 ( $cust_bill_pkg->setup != 0
2007 ? sprintf("%.2f", $cust_bill_pkg->setup )
2009 ( $cust_bill_pkg->recur != 0
2010 ? sprintf("%.2f", $cust_bill_pkg->recur )
2012 ( $cust_bill_pkg->sdate
2013 ? time2str("%x", $cust_bill_pkg->sdate)
2015 ($cust_bill_pkg->edate
2016 ?time2str("%x", $cust_bill_pkg->edate)
2020 } else { #pkgnum tax
2021 next unless $cust_bill_pkg->setup != 0;
2022 $pkg = $cust_bill_pkg->desc;
2023 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2024 ( $sdate, $edate ) = ( '', '' );
2030 ( map { '' } (1..11) ),
2031 ($pkg, $setup, $recur, $sdate, $edate)
2032 ) or die "can't create csv";
2034 $detail .= $csv->string. "\n";
2040 ( $header, $detail );
2046 Pays this invoice with a compliemntary payment. If there is an error,
2047 returns the error, otherwise returns false.
2053 my $cust_pay = new FS::cust_pay ( {
2054 'invnum' => $self->invnum,
2055 'paid' => $self->owed,
2058 'payinfo' => $self->cust_main->payinfo,
2066 Attempts to pay this invoice with a credit card payment via a
2067 Business::OnlinePayment realtime gateway. See
2068 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2069 for supported processors.
2075 $self->realtime_bop( 'CC', @_ );
2080 Attempts to pay this invoice with an electronic check (ACH) payment via a
2081 Business::OnlinePayment realtime gateway. See
2082 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2083 for supported processors.
2089 $self->realtime_bop( 'ECHECK', @_ );
2094 Attempts to pay this invoice with phone bill (LEC) payment via a
2095 Business::OnlinePayment realtime gateway. See
2096 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2097 for supported processors.
2103 $self->realtime_bop( 'LEC', @_ );
2107 my( $self, $method ) = (shift,shift);
2108 my $conf = $self->conf;
2111 my $cust_main = $self->cust_main;
2112 my $balance = $cust_main->balance;
2113 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2114 $amount = sprintf("%.2f", $amount);
2115 return "not run (balance $balance)" unless $amount > 0;
2117 my $description = 'Internet Services';
2118 if ( $conf->exists('business-onlinepayment-description') ) {
2119 my $dtempl = $conf->config('business-onlinepayment-description');
2121 my $agent_obj = $cust_main->agent
2122 or die "can't retreive agent for $cust_main (agentnum ".
2123 $cust_main->agentnum. ")";
2124 my $agent = $agent_obj->agent;
2125 my $pkgs = join(', ',
2126 map { $_->part_pkg->pkg }
2127 grep { $_->pkgnum } $self->cust_bill_pkg
2129 $description = eval qq("$dtempl");
2132 $cust_main->realtime_bop($method, $amount,
2133 'description' => $description,
2134 'invnum' => $self->invnum,
2135 #this didn't do what we want, it just calls apply_payments_and_credits
2137 'apply_to_invoice' => 1,
2140 #this changes application behavior: auto payments
2141 #triggered against a specific invoice are now applied
2142 #to that invoice instead of oldest open.
2148 =item batch_card OPTION => VALUE...
2150 Adds a payment for this invoice to the pending credit card batch (see
2151 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2152 runs the payment using a realtime gateway.
2157 my ($self, %options) = @_;
2158 my $cust_main = $self->cust_main;
2160 $options{invnum} = $self->invnum;
2162 $cust_main->batch_card(%options);
2165 sub _agent_template {
2167 $self->cust_main->agent_template;
2170 sub _agent_invoice_from {
2172 $self->cust_main->agent_invoice_from;
2175 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2177 Returns an text invoice, as a list of lines.
2179 Options can be passed as a hashref (recommended) or as a list of time, template
2180 and then any key/value pairs for any other options.
2182 I<time>, if specified, is used to control the printing of overdue messages. The
2183 default is now. It isn't the date of the invoice; that's the `_date' field.
2184 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2185 L<Time::Local> and L<Date::Parse> for conversion functions.
2187 I<template>, if specified, is the name of a suffix for alternate invoices.
2189 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2195 my( $today, $template, %opt );
2197 %opt = %{ shift() };
2198 $today = delete($opt{'time'}) || '';
2199 $template = delete($opt{template}) || '';
2201 ( $today, $template, %opt ) = @_;
2204 my %params = ( 'format' => 'template' );
2205 $params{'time'} = $today if $today;
2206 $params{'template'} = $template if $template;
2207 $params{$_} = $opt{$_}
2208 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2210 $self->print_generic( %params );
2213 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2215 Internal method - returns a filename of a filled-in LaTeX template for this
2216 invoice (Note: add ".tex" to get the actual filename), and a filename of
2217 an associated logo (with the .eps extension included).
2219 See print_ps and print_pdf for methods that return PostScript and PDF output.
2221 Options can be passed as a hashref (recommended) or as a list of time, template
2222 and then any key/value pairs for any other options.
2224 I<time>, if specified, is used to control the printing of overdue messages. The
2225 default is now. It isn't the date of the invoice; that's the `_date' field.
2226 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2227 L<Time::Local> and L<Date::Parse> for conversion functions.
2229 I<template>, if specified, is the name of a suffix for alternate invoices.
2231 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2237 my $conf = $self->conf;
2238 my( $today, $template, %opt );
2240 %opt = %{ shift() };
2241 $today = delete($opt{'time'}) || '';
2242 $template = delete($opt{template}) || '';
2244 ( $today, $template, %opt ) = @_;
2247 my %params = ( 'format' => 'latex' );
2248 $params{'time'} = $today if $today;
2249 $params{'template'} = $template if $template;
2250 $params{$_} = $opt{$_}
2251 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2253 $template ||= $self->_agent_template;
2255 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2256 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2260 ) or die "can't open temp file: $!\n";
2262 my $agentnum = $self->cust_main->agentnum;
2264 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2265 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2266 or die "can't write temp file: $!\n";
2268 print $lh $conf->config_binary('logo.eps', $agentnum)
2269 or die "can't write temp file: $!\n";
2272 $params{'logo_file'} = $lh->filename;
2274 if($conf->exists('invoice-barcode')){
2275 my $png_file = $self->invoice_barcode($dir);
2276 my $eps_file = $png_file;
2277 $eps_file =~ s/\.png$/.eps/g;
2278 $png_file =~ /(barcode.*png)/;
2280 $eps_file =~ /(barcode.*eps)/;
2283 my $curr_dir = cwd();
2285 # after painfuly long experimentation, it was determined that sam2p won't
2286 # accept : and other chars in the path, no matter how hard I tried to
2287 # escape them, hence the chdir (and chdir back, just to be safe)
2288 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2289 or die "sam2p failed: $!\n";
2293 $params{'barcode_file'} = $eps_file;
2296 my @filled_in = $self->print_generic( %params );
2298 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2302 ) or die "can't open temp file: $!\n";
2303 binmode($fh, ':utf8'); # language support
2304 print $fh join('', @filled_in );
2307 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2308 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2312 =item invoice_barcode DIR_OR_FALSE
2314 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2315 it is taken as the temp directory where the PNG file will be generated and the
2316 PNG file name is returned. Otherwise, the PNG image itself is returned.
2320 sub invoice_barcode {
2321 my ($self, $dir) = (shift,shift);
2323 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2324 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2325 my $gd = $gdbar->plot(Height => 30);
2328 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2332 ) or die "can't open temp file: $!\n";
2333 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2334 my $png_file = $bh->filename;
2341 =item print_generic OPTION => VALUE ...
2343 Internal method - returns a filled-in template for this invoice as a scalar.
2345 See print_ps and print_pdf for methods that return PostScript and PDF output.
2347 Non optional options include
2348 format - latex, html, template
2350 Optional options include
2352 template - a value used as a suffix for a configuration template
2354 time - a value used to control the printing of overdue messages. The
2355 default is now. It isn't the date of the invoice; that's the `_date' field.
2356 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2357 L<Time::Local> and L<Date::Parse> for conversion functions.
2361 unsquelch_cdr - overrides any per customer cdr squelching when true
2363 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2365 locale - override customer's locale
2369 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2370 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2371 # yes: fixed width/plain text printing will be borked
2373 my( $self, %params ) = @_;
2374 my $conf = $self->conf;
2375 my $today = $params{today} ? $params{today} : time;
2376 warn "$me print_generic called on $self with suffix $params{template}\n"
2379 my $format = $params{format};
2380 die "Unknown format: $format"
2381 unless $format =~ /^(latex|html|template)$/;
2383 my $cust_main = $self->cust_main;
2384 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2385 unless $cust_main->payname
2386 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2388 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2389 'html' => [ '<%=', '%>' ],
2390 'template' => [ '{', '}' ],
2393 warn "$me print_generic creating template\n"
2396 #create the template
2397 my $template = $params{template} ? $params{template} : $self->_agent_template;
2398 my $templatefile = "invoice_$format";
2399 $templatefile .= "_$template"
2400 if length($template) && $conf->exists($templatefile."_$template");
2401 my @invoice_template = map "$_\n", $conf->config($templatefile)
2402 or die "cannot load config data $templatefile";
2405 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2406 #change this to a die when the old code is removed
2407 warn "old-style invoice template $templatefile; ".
2408 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2409 $old_latex = 'true';
2410 @invoice_template = _translate_old_latex_format(@invoice_template);
2413 warn "$me print_generic creating T:T object\n"
2416 my $text_template = new Text::Template(
2418 SOURCE => \@invoice_template,
2419 DELIMITERS => $delimiters{$format},
2422 warn "$me print_generic compiling T:T object\n"
2425 $text_template->compile()
2426 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2429 # additional substitution could possibly cause breakage in existing templates
2430 my %convert_maps = (
2432 'notes' => sub { map "$_", @_ },
2433 'footer' => sub { map "$_", @_ },
2434 'smallfooter' => sub { map "$_", @_ },
2435 'returnaddress' => sub { map "$_", @_ },
2436 'coupon' => sub { map "$_", @_ },
2437 'summary' => sub { map "$_", @_ },
2443 s/%%(.*)$/<!-- $1 -->/g;
2444 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2445 s/\\begin\{enumerate\}/<ol>/g;
2447 s/\\end\{enumerate\}/<\/ol>/g;
2448 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2457 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2459 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2464 s/\\\\\*?\s*$/<BR>/;
2465 s/\\hyphenation\{[\w\s\-]+}//;
2470 'coupon' => sub { "" },
2471 'summary' => sub { "" },
2478 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2479 s/\\begin\{enumerate\}//g;
2481 s/\\end\{enumerate\}//g;
2482 s/\\textbf\{(.*)\}/$1/g;
2489 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2491 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2496 s/\\\\\*?\s*$/\n/; # dubious
2497 s/\\hyphenation\{[\w\s\-]+}//;
2501 'coupon' => sub { "" },
2502 'summary' => sub { "" },
2507 # hashes for differing output formats
2508 my %nbsps = ( 'latex' => '~',
2509 'html' => '', # '&nbps;' would be nice
2510 'template' => '', # not used
2512 my $nbsp = $nbsps{$format};
2514 my %escape_functions = ( 'latex' => \&_latex_escape,
2515 'html' => \&_html_escape_nbsp,#\&encode_entities,
2516 'template' => sub { shift },
2518 my $escape_function = $escape_functions{$format};
2519 my $escape_function_nonbsp = ($format eq 'html')
2520 ? \&_html_escape : $escape_function;
2522 my %date_formats = ( 'latex' => $date_format_long,
2523 'html' => $date_format_long,
2526 $date_formats{'html'} =~ s/ / /g;
2528 my $date_format = $date_formats{$format};
2530 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2532 'html' => sub { return '<b>'. shift(). '</b>'
2534 'template' => sub { shift },
2536 my $embolden_function = $embolden_functions{$format};
2538 my %newline_tokens = ( 'latex' => '\\\\',
2542 my $newline_token = $newline_tokens{$format};
2544 warn "$me generating template variables\n"
2547 # generate template variables
2550 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2554 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2560 $returnaddress = join("\n",
2561 $conf->config_orbase("invoice_${format}returnaddress", $template)
2564 } elsif ( grep /\S/,
2565 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2567 my $convert_map = $convert_maps{$format}{'returnaddress'};
2570 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2575 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2577 my $convert_map = $convert_maps{$format}{'returnaddress'};
2578 $returnaddress = join( "\n", &$convert_map(
2579 map { s/( {2,})/'~' x length($1)/eg;
2583 ( $conf->config('company_name', $self->cust_main->agentnum),
2584 $conf->config('company_address', $self->cust_main->agentnum),
2591 my $warning = "Couldn't find a return address; ".
2592 "do you need to set the company_address configuration value?";
2594 $returnaddress = $nbsp;
2595 #$returnaddress = $warning;
2599 warn "$me generating invoice data\n"
2602 my $agentnum = $self->cust_main->agentnum;
2604 my %invoice_data = (
2607 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2608 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2609 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2610 'returnaddress' => $returnaddress,
2611 'agent' => &$escape_function($cust_main->agent->agent),
2614 'invnum' => $self->invnum,
2615 'date' => time2str($date_format, $self->_date),
2616 'today' => time2str($date_format_long, $today),
2617 'terms' => $self->terms,
2618 'template' => $template, #params{'template'},
2619 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2620 'current_charges' => sprintf("%.2f", $self->charged),
2621 'duedate' => $self->due_date2str($rdate_format), #date_format?
2624 'custnum' => $cust_main->display_custnum,
2625 'agent_custid' => &$escape_function($cust_main->agent_custid),
2626 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2627 payname company address1 address2 city state zip fax
2631 'ship_enable' => $conf->exists('invoice-ship_address'),
2632 'unitprices' => $conf->exists('invoice-unitprice'),
2633 'smallernotes' => $conf->exists('invoice-smallernotes'),
2634 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2635 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2637 #layout info -- would be fancy to calc some of this and bury the template
2639 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2640 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2641 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2642 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2643 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2644 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2645 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2646 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2647 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2648 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2650 # better hang on to conf_dir for a while (for old templates)
2651 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2653 #these are only used when doing paged plaintext
2660 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2661 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2662 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2663 # eval to avoid death for unimplemented languages
2664 my $dh = eval { Date::Language->new($info{'name'}) } ||
2665 Date::Language->new(); # fall back to English
2666 # prototype here to silence warnings
2667 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2668 # eventually use this date handle everywhere in here, too
2670 my $min_sdate = 999999999999;
2672 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2673 next unless $cust_bill_pkg->pkgnum > 0;
2674 $min_sdate = $cust_bill_pkg->sdate
2675 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2676 $max_edate = $cust_bill_pkg->edate
2677 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2680 $invoice_data{'bill_period'} = '';
2681 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2682 . " to " . time2str('%e %h', $max_edate)
2683 if ($max_edate != 0 && $min_sdate != 999999999999);
2685 $invoice_data{finance_section} = '';
2686 if ( $conf->config('finance_pkgclass') ) {
2688 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2689 $invoice_data{finance_section} = $pkg_class->categoryname;
2691 $invoice_data{finance_amount} = '0.00';
2692 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2694 my $countrydefault = $conf->config('countrydefault') || 'US';
2695 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2696 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2697 my $method = $prefix.$_;
2698 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2700 $invoice_data{'ship_country'} = ''
2701 if ( $invoice_data{'ship_country'} eq $countrydefault );
2703 $invoice_data{'cid'} = $params{'cid'}
2706 if ( $cust_main->country eq $countrydefault ) {
2707 $invoice_data{'country'} = '';
2709 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2713 $invoice_data{'address'} = \@address;
2715 $cust_main->payname.
2716 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2717 ? " (P.O. #". $cust_main->payinfo. ")"
2721 push @address, $cust_main->company
2722 if $cust_main->company;
2723 push @address, $cust_main->address1;
2724 push @address, $cust_main->address2
2725 if $cust_main->address2;
2727 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2728 push @address, $invoice_data{'country'}
2729 if $invoice_data{'country'};
2731 while (scalar(@address) < 5);
2733 $invoice_data{'logo_file'} = $params{'logo_file'}
2734 if $params{'logo_file'};
2735 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2736 if $params{'barcode_file'};
2737 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2738 if $params{'barcode_img'};
2739 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2740 if $params{'barcode_cid'};
2742 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2743 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2744 #my $balance_due = $self->owed + $pr_total - $cr_total;
2745 my $balance_due = $self->owed + $pr_total;
2747 # the customer's current balance as shown on the invoice before this one
2748 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2750 # the change in balance from that invoice to this one
2751 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2753 # the sum of amount owed on all previous invoices
2754 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2756 # the sum of amount owed on all invoices
2757 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2759 # info from customer's last invoice before this one, for some
2761 $invoice_data{'last_bill'} = {};
2762 my $last_bill = $pr_cust_bill[-1];
2764 $invoice_data{'last_bill'} = {
2765 '_date' => $last_bill->_date, #unformatted
2766 # all we need for now
2770 my $summarypage = '';
2771 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2774 $invoice_data{'summarypage'} = $summarypage;
2776 warn "$me substituting variables in notes, footer, smallfooter\n"
2779 my @include = (qw( notes footer smallfooter ));
2780 push @include, 'coupon' unless $params{'no_coupon'};
2781 foreach my $include (@include) {
2783 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2786 if ( $conf->exists($inc_file, $agentnum)
2787 && length( $conf->config($inc_file, $agentnum) ) ) {
2789 @inc_src = $conf->config($inc_file, $agentnum);
2793 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2795 my $convert_map = $convert_maps{$format}{$include};
2797 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2798 s/--\@\]/$delimiters{$format}[1]/g;
2801 &$convert_map( $conf->config($inc_file, $agentnum) );
2805 my $inc_tt = new Text::Template (
2807 SOURCE => [ map "$_\n", @inc_src ],
2808 DELIMITERS => $delimiters{$format},
2809 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2811 unless ( $inc_tt->compile() ) {
2812 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2813 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2817 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2819 $invoice_data{$include} =~ s/\n+$//
2820 if ($format eq 'latex');
2823 # let invoices use either of these as needed
2824 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2825 ? $cust_main->payinfo : '';
2826 $invoice_data{'po_line'} =
2827 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2828 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2831 my %money_chars = ( 'latex' => '',
2832 'html' => $conf->config('money_char') || '$',
2835 my $money_char = $money_chars{$format};
2837 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2838 'html' => $conf->config('money_char') || '$',
2841 my $other_money_char = $other_money_chars{$format};
2842 $invoice_data{'dollar'} = $other_money_char;
2844 my @detail_items = ();
2845 my @total_items = ();
2849 $invoice_data{'detail_items'} = \@detail_items;
2850 $invoice_data{'total_items'} = \@total_items;
2851 $invoice_data{'buf'} = \@buf;
2852 $invoice_data{'sections'} = \@sections;
2854 warn "$me generating sections\n"
2857 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2858 'subtotal' => $other_money_char.
2859 sprintf('%.2f', $pr_total),
2860 'summarized' => '', #why? $summarypage ? 'Y' : '',
2862 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2863 join(' / ', map { $cust_main->balance_date_range(@$_) }
2864 $self->_prior_month30s
2866 if $conf->exists('invoice_include_aging');
2869 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2870 'subtotal' => $taxtotal, # adjusted below
2872 my $tax_weight = _pkg_category($tax_section->{description})
2873 ? _pkg_category($tax_section->{description})->weight
2875 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2876 $tax_section->{'sort_weight'} = $tax_weight;
2879 my $adjusttotal = 0;
2880 my $adjust_section = { 'description' =>
2881 $self->mt('Credits, Payments, and Adjustments'),
2882 'subtotal' => 0, # adjusted below
2884 my $adjust_weight = _pkg_category($adjust_section->{description})
2885 ? _pkg_category($adjust_section->{description})->weight
2887 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2888 $adjust_section->{'sort_weight'} = $adjust_weight;
2890 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2891 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2892 $invoice_data{'multisection'} = $multisection;
2893 my $late_sections = [];
2894 my $extra_sections = [];
2895 my $extra_lines = ();
2896 if ( $multisection ) {
2897 ($extra_sections, $extra_lines) =
2898 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2899 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2901 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2903 push @detail_items, @$extra_lines if $extra_lines;
2905 $self->_items_sections( $late_sections, # this could stand a refactor
2907 $escape_function_nonbsp,
2911 if ($conf->exists('svc_phone_sections')) {
2912 my ($phone_sections, $phone_lines) =
2913 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2914 push @{$late_sections}, @$phone_sections;
2915 push @detail_items, @$phone_lines;
2917 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2918 my ($accountcode_section, $accountcode_lines) =
2919 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2920 if ( scalar(@$accountcode_lines) ) {
2921 push @{$late_sections}, $accountcode_section;
2922 push @detail_items, @$accountcode_lines;
2925 } else {# not multisection
2926 # make a default section
2927 push @sections, { 'description' => '', 'subtotal' => '',
2928 'no_subtotal' => 1 };
2929 # and calculate the finance charge total, since it won't get done otherwise.
2930 # XXX possibly other totals?
2931 # XXX possibly finance_pkgclass should not be used in this manner?
2932 if ( $conf->exists('finance_pkgclass') ) {
2933 my @finance_charges;
2934 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2935 if ( grep { $_->section eq $invoice_data{finance_section} }
2936 $cust_bill_pkg->cust_bill_pkg_display ) {
2937 # I think these are always setup fees, but just to be sure...
2938 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2941 $invoice_data{finance_amount} =
2942 sprintf('%.2f', sum( @finance_charges ) || 0);
2946 unless ( $conf->exists('disable_previous_balance')
2947 || $conf->exists('previous_balance-summary_only')
2951 warn "$me adding previous balances\n"
2954 foreach my $line_item ( $self->_items_previous ) {
2957 ext_description => [],
2959 $detail->{'ref'} = $line_item->{'pkgnum'};
2960 $detail->{'quantity'} = 1;
2961 $detail->{'section'} = $previous_section;
2962 $detail->{'description'} = &$escape_function($line_item->{'description'});
2963 if ( exists $line_item->{'ext_description'} ) {
2964 @{$detail->{'ext_description'}} = map {
2965 &$escape_function($_);
2966 } @{$line_item->{'ext_description'}};
2968 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2969 $line_item->{'amount'};
2970 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2972 push @detail_items, $detail;
2973 push @buf, [ $detail->{'description'},
2974 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2980 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2981 push @buf, ['','-----------'];
2982 push @buf, [ $self->mt('Total Previous Balance'),
2983 $money_char. sprintf("%10.2f", $pr_total) ];
2987 if ( $conf->exists('svc_phone-did-summary') ) {
2988 warn "$me adding DID summary\n"
2991 my ($didsummary,$minutes) = $self->_did_summary;
2992 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2994 { 'description' => $didsummary_desc,
2995 'ext_description' => [ $didsummary, $minutes ],
2999 foreach my $section (@sections, @$late_sections) {
3001 warn "$me adding section \n". Dumper($section)
3004 # begin some normalization
3005 $section->{'subtotal'} = $section->{'amount'}
3007 && !exists($section->{subtotal})
3008 && exists($section->{amount});
3010 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3011 if ( $invoice_data{finance_section} &&
3012 $section->{'description'} eq $invoice_data{finance_section} );
3014 $section->{'subtotal'} = $other_money_char.
3015 sprintf('%.2f', $section->{'subtotal'})
3018 # continue some normalization
3019 $section->{'amount'} = $section->{'subtotal'}
3023 if ( $section->{'description'} ) {
3024 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3029 warn "$me setting options\n"
3032 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3034 $options{'section'} = $section if $multisection;
3035 $options{'format'} = $format;
3036 $options{'escape_function'} = $escape_function;
3037 $options{'format_function'} = sub { () } unless $unsquelched;
3038 $options{'unsquelched'} = $unsquelched;
3039 $options{'summary_page'} = $summarypage;
3040 $options{'skip_usage'} =
3041 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3042 $options{'multilocation'} = $multilocation;
3043 $options{'multisection'} = $multisection;
3045 warn "$me searching for line items\n"
3048 foreach my $line_item ( $self->_items_pkg(%options) ) {
3050 warn "$me adding line item $line_item\n"
3054 ext_description => [],
3056 $detail->{'ref'} = $line_item->{'pkgnum'};
3057 $detail->{'quantity'} = $line_item->{'quantity'};
3058 $detail->{'section'} = $section;
3059 $detail->{'description'} = &$escape_function($line_item->{'description'});
3060 if ( exists $line_item->{'ext_description'} ) {
3061 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3063 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3064 $line_item->{'amount'};
3065 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3066 $line_item->{'unit_amount'};
3067 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3069 $detail->{'sdate'} = $line_item->{'sdate'};
3070 $detail->{'edate'} = $line_item->{'edate'};
3071 $detail->{'seconds'} = $line_item->{'seconds'};
3073 push @detail_items, $detail;
3074 push @buf, ( [ $detail->{'description'},
3075 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3077 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3081 if ( $section->{'description'} ) {
3082 push @buf, ( ['','-----------'],
3083 [ $section->{'description'}. ' sub-total',
3084 $section->{'subtotal'} # already formatted this
3093 $invoice_data{current_less_finance} =
3094 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3096 if ( $multisection && !$conf->exists('disable_previous_balance')
3097 || $conf->exists('previous_balance-summary_only') )
3099 unshift @sections, $previous_section if $pr_total;
3102 warn "$me adding taxes\n"
3105 foreach my $tax ( $self->_items_tax ) {
3107 $taxtotal += $tax->{'amount'};
3109 my $description = &$escape_function( $tax->{'description'} );
3110 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3112 if ( $multisection ) {
3114 my $money = $old_latex ? '' : $money_char;
3115 push @detail_items, {
3116 ext_description => [],
3119 description => $description,
3120 amount => $money. $amount,
3122 section => $tax_section,
3127 push @total_items, {
3128 'total_item' => $description,
3129 'total_amount' => $other_money_char. $amount,
3134 push @buf,[ $description,
3135 $money_char. $amount,
3142 $total->{'total_item'} = $self->mt('Sub-total');
3143 $total->{'total_amount'} =
3144 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3146 if ( $multisection ) {
3147 $tax_section->{'subtotal'} = $other_money_char.
3148 sprintf('%.2f', $taxtotal);
3149 $tax_section->{'pretotal'} = 'New charges sub-total '.
3150 $total->{'total_amount'};
3151 push @sections, $tax_section if $taxtotal;
3153 unshift @total_items, $total;
3156 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3158 push @buf,['','-----------'];
3159 push @buf,[$self->mt(
3160 $conf->exists('disable_previous_balance')
3162 : 'Total New Charges'
3164 $money_char. sprintf("%10.2f",$self->charged) ];
3170 $item = $conf->config('previous_balance-exclude_from_total')
3171 || 'Total New Charges'
3172 if $conf->exists('previous_balance-exclude_from_total');
3173 my $amount = $self->charged +
3174 ( $conf->exists('disable_previous_balance') ||
3175 $conf->exists('previous_balance-exclude_from_total')
3179 $total->{'total_item'} = &$embolden_function($self->mt($item));
3180 $total->{'total_amount'} =
3181 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3182 if ( $multisection ) {
3183 if ( $adjust_section->{'sort_weight'} ) {
3184 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3185 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3187 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3188 $other_money_char. sprintf('%.2f', $self->charged );
3191 push @total_items, $total;
3193 push @buf,['','-----------'];
3196 sprintf( '%10.2f', $amount )
3201 unless ( $conf->exists('disable_previous_balance') ) {
3202 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3205 my $credittotal = 0;
3206 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3209 $total->{'total_item'} = &$escape_function($credit->{'description'});
3210 $credittotal += $credit->{'amount'};
3211 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3212 $adjusttotal += $credit->{'amount'};
3213 if ( $multisection ) {
3214 my $money = $old_latex ? '' : $money_char;
3215 push @detail_items, {
3216 ext_description => [],
3219 description => &$escape_function($credit->{'description'}),
3220 amount => $money. $credit->{'amount'},
3222 section => $adjust_section,
3225 push @total_items, $total;
3229 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3232 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3233 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3237 my $paymenttotal = 0;
3238 foreach my $payment ( $self->_items_payments ) {
3240 $total->{'total_item'} = &$escape_function($payment->{'description'});
3241 $paymenttotal += $payment->{'amount'};
3242 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3243 $adjusttotal += $payment->{'amount'};
3244 if ( $multisection ) {
3245 my $money = $old_latex ? '' : $money_char;
3246 push @detail_items, {
3247 ext_description => [],
3250 description => &$escape_function($payment->{'description'}),
3251 amount => $money. $payment->{'amount'},
3253 section => $adjust_section,
3256 push @total_items, $total;
3258 push @buf, [ $payment->{'description'},
3259 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3262 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3264 if ( $multisection ) {
3265 $adjust_section->{'subtotal'} = $other_money_char.
3266 sprintf('%.2f', $adjusttotal);
3267 push @sections, $adjust_section
3268 unless $adjust_section->{sort_weight};
3271 # create Balance Due message
3274 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3275 $total->{'total_amount'} =
3276 &$embolden_function(
3277 $other_money_char. sprintf('%.2f', $summarypage
3279 $self->billing_balance
3280 : $self->owed + $pr_total
3283 if ( $multisection && !$adjust_section->{sort_weight} ) {
3284 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3285 $total->{'total_amount'};
3287 push @total_items, $total;
3289 push @buf,['','-----------'];
3290 push @buf,[$self->balance_due_msg, $money_char.
3291 sprintf("%10.2f", $balance_due ) ];
3294 if ( $conf->exists('previous_balance-show_credit')
3295 and $cust_main->balance < 0 ) {
3296 my $credit_total = {
3297 'total_item' => &$embolden_function($self->credit_balance_msg),
3298 'total_amount' => &$embolden_function(
3299 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3302 if ( $multisection ) {
3303 $adjust_section->{'posttotal'} .= $newline_token .
3304 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3307 push @total_items, $credit_total;
3309 push @buf,['','-----------'];
3310 push @buf,[$self->credit_balance_msg, $money_char.
3311 sprintf("%10.2f", -$cust_main->balance ) ];
3315 if ( $multisection ) {
3316 if ($conf->exists('svc_phone_sections')) {
3318 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3319 $total->{'total_amount'} =
3320 &$embolden_function(
3321 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3323 my $last_section = pop @sections;
3324 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3325 $total->{'total_amount'};
3326 push @sections, $last_section;
3328 push @sections, @$late_sections
3332 # make a discounts-available section, even without multisection
3333 if ( $conf->exists('discount-show_available')
3334 and my @discounts_avail = $self->_items_discounts_avail ) {
3335 my $discount_section = {
3336 'description' => $self->mt('Discounts Available'),
3341 push @sections, $discount_section;
3342 push @detail_items, map { +{
3343 'ref' => '', #should this be something else?
3344 'section' => $discount_section,
3345 'description' => &$escape_function( $_->{description} ),
3346 'amount' => $money_char . &$escape_function( $_->{amount} ),
3347 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3348 } } @discounts_avail;
3351 # All sections and items are built; now fill in templates.
3352 my @includelist = ();
3353 push @includelist, 'summary' if $summarypage;
3354 foreach my $include ( @includelist ) {
3356 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3359 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3361 @inc_src = $conf->config($inc_file, $agentnum);
3365 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3367 my $convert_map = $convert_maps{$format}{$include};
3369 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3370 s/--\@\]/$delimiters{$format}[1]/g;
3373 &$convert_map( $conf->config($inc_file, $agentnum) );
3377 my $inc_tt = new Text::Template (
3379 SOURCE => [ map "$_\n", @inc_src ],
3380 DELIMITERS => $delimiters{$format},
3381 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3383 unless ( $inc_tt->compile() ) {
3384 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3385 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3389 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3391 $invoice_data{$include} =~ s/\n+$//
3392 if ($format eq 'latex');
3397 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3398 /invoice_lines\((\d*)\)/;
3399 $invoice_lines += $1 || scalar(@buf);
3402 die "no invoice_lines() functions in template?"
3403 if ( $format eq 'template' && !$wasfunc );
3405 if ($format eq 'template') {
3407 if ( $invoice_lines ) {
3408 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3409 $invoice_data{'total_pages'}++
3410 if scalar(@buf) % $invoice_lines;
3413 #setup subroutine for the template
3414 $invoice_data{invoice_lines} = sub {
3415 my $lines = shift || scalar(@buf);
3427 push @collect, split("\n",
3428 $text_template->fill_in( HASH => \%invoice_data )
3430 $invoice_data{'page'}++;
3432 map "$_\n", @collect;
3434 # this is where we actually create the invoice
3435 warn "filling in template for invoice ". $self->invnum. "\n"
3437 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3440 $text_template->fill_in(HASH => \%invoice_data);
3444 # helper routine for generating date ranges
3445 sub _prior_month30s {
3448 [ 1, 2592000 ], # 0-30 days ago
3449 [ 2592000, 5184000 ], # 30-60 days ago
3450 [ 5184000, 7776000 ], # 60-90 days ago
3451 [ 7776000, 0 ], # 90+ days ago
3454 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3455 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3460 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3462 Returns an postscript invoice, as a scalar.
3464 Options can be passed as a hashref (recommended) or as a list of time, template
3465 and then any key/value pairs for any other options.
3467 I<time> an optional value used to control the printing of overdue messages. The
3468 default is now. It isn't the date of the invoice; that's the `_date' field.
3469 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3470 L<Time::Local> and L<Date::Parse> for conversion functions.
3472 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3479 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3480 my $ps = generate_ps($file);
3482 unlink($barcodefile) if $barcodefile;
3487 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3489 Returns an PDF invoice, as a scalar.
3491 Options can be passed as a hashref (recommended) or as a list of time, template
3492 and then any key/value pairs for any other options.
3494 I<time> an optional value used to control the printing of overdue messages. The
3495 default is now. It isn't the date of the invoice; that's the `_date' field.
3496 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3497 L<Time::Local> and L<Date::Parse> for conversion functions.
3499 I<template>, if specified, is the name of a suffix for alternate invoices.
3501 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3508 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3509 my $pdf = generate_pdf($file);
3511 unlink($barcodefile) if $barcodefile;
3516 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3518 Returns an HTML invoice, as a scalar.
3520 I<time> an optional value used to control the printing of overdue messages. The
3521 default is now. It isn't the date of the invoice; that's the `_date' field.
3522 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3523 L<Time::Local> and L<Date::Parse> for conversion functions.
3525 I<template>, if specified, is the name of a suffix for alternate invoices.
3527 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3529 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3530 when emailing the invoice as part of a multipart/related MIME email.
3538 %params = %{ shift() };
3540 $params{'time'} = shift;
3541 $params{'template'} = shift;
3542 $params{'cid'} = shift;
3545 $params{'format'} = 'html';
3547 $self->print_generic( %params );
3550 # quick subroutine for print_latex
3552 # There are ten characters that LaTeX treats as special characters, which
3553 # means that they do not simply typeset themselves:
3554 # # $ % & ~ _ ^ \ { }
3556 # TeX ignores blanks following an escaped character; if you want a blank (as
3557 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3561 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3562 $value =~ s/([<>])/\$$1\$/g;
3568 encode_entities($value);
3572 sub _html_escape_nbsp {
3573 my $value = _html_escape(shift);
3574 $value =~ s/ +/ /g;
3578 #utility methods for print_*
3580 sub _translate_old_latex_format {
3581 warn "_translate_old_latex_format called\n"
3588 if ( $line =~ /^%%Detail\s*$/ ) {
3590 push @template, q![@--!,
3591 q! foreach my $_tr_line (@detail_items) {!,
3592 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3593 q! $_tr_line->{'description'} .= !,
3594 q! "\\tabularnewline\n~~".!,
3595 q! join( "\\tabularnewline\n~~",!,
3596 q! @{$_tr_line->{'ext_description'}}!,
3600 while ( ( my $line_item_line = shift )
3601 !~ /^%%EndDetail\s*$/ ) {
3602 $line_item_line =~ s/'/\\'/g; # nice LTS
3603 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3604 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3605 push @template, " \$OUT .= '$line_item_line';";
3608 push @template, '}',
3611 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3613 push @template, '[@--',
3614 ' foreach my $_tr_line (@total_items) {';
3616 while ( ( my $total_item_line = shift )
3617 !~ /^%%EndTotalDetails\s*$/ ) {
3618 $total_item_line =~ s/'/\\'/g; # nice LTS
3619 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3620 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3621 push @template, " \$OUT .= '$total_item_line';";
3624 push @template, '}',
3628 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3629 push @template, $line;
3635 warn "$_\n" foreach @template;
3643 my $conf = $self->conf;
3645 #check for an invoice-specific override
3646 return $self->invoice_terms if $self->invoice_terms;
3648 #check for a customer- specific override
3649 my $cust_main = $self->cust_main;
3650 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3652 #use configured default
3653 $conf->config('invoice_default_terms') || '';
3659 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3660 $duedate = $self->_date() + ( $1 * 86400 );
3667 $self->due_date ? time2str(shift, $self->due_date) : '';
3670 sub balance_due_msg {
3672 my $msg = $self->mt('Balance Due');
3673 return $msg unless $self->terms;
3674 if ( $self->due_date ) {
3675 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3676 $self->due_date2str($date_format);
3677 } elsif ( $self->terms ) {
3678 $msg .= ' - '. $self->terms;
3683 sub balance_due_date {
3685 my $conf = $self->conf;
3687 if ( $conf->exists('invoice_default_terms')
3688 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3689 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3694 sub credit_balance_msg {
3696 $self->mt('Credit Balance Remaining')
3699 =item invnum_date_pretty
3701 Returns a string with the invoice number and date, for example:
3702 "Invoice #54 (3/20/2008)"
3706 sub invnum_date_pretty {
3708 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3713 Returns a string with the date, for example: "3/20/2008"
3719 time2str($date_format, $self->_date);
3722 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3724 Generate section information for all items appearing on this invoice.
3725 This will only be called for multi-section invoices.
3727 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3728 related display records (L<FS::cust_bill_pkg_display>) and organize
3729 them into two groups ("early" and "late" according to whether they come
3730 before or after the total), then into sections. A subtotal is calculated
3733 Section descriptions are returned in sort weight order. Each consists
3734 of a hash containing:
3736 description: the package category name, escaped
3737 subtotal: the total charges in that section
3738 tax_section: a flag indicating that the section contains only tax charges
3739 summarized: same as tax_section, for some reason
3740 sort_weight: the package category's sort weight
3742 If 'condense' is set on the display record, it also contains everything
3743 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3744 coderefs to generate parts of the invoice. This is not advised.
3748 LATE: an arrayref to push the "late" section hashes onto. The "early"
3749 group is simply returned from the method.
3751 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3752 Turning this on has the following effects:
3753 - Ignores display items with the 'summary' flag.
3754 - Combines all items into the "early" group.
3755 - Creates sections for all non-disabled package categories, even if they
3756 have no charges on this invoice, as well as a section with no name.
3758 ESCAPE: an escape function to use for section titles.
3760 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3761 sorted list. If there are any of these, section subtotals exclude
3764 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3765 passed through to C<_condense_section()>.
3769 use vars qw(%pkg_category_cache);
3770 sub _items_sections {
3773 my $summarypage = shift;
3775 my $extra_sections = shift;
3779 my %late_subtotal = ();
3782 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3785 my $usage = $cust_bill_pkg->usage;
3787 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3788 next if ( $display->summary && $summarypage );
3790 my $section = $display->section;
3791 my $type = $display->type;
3793 $not_tax{$section} = 1
3794 unless $cust_bill_pkg->pkgnum == 0;
3796 if ( $display->post_total && !$summarypage ) {
3797 if (! $type || $type eq 'S') {
3798 $late_subtotal{$section} += $cust_bill_pkg->setup
3799 if $cust_bill_pkg->setup != 0;
3803 $late_subtotal{$section} += $cust_bill_pkg->recur
3804 if $cust_bill_pkg->recur != 0;
3807 if ($type && $type eq 'R') {
3808 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3809 if $cust_bill_pkg->recur != 0;
3812 if ($type && $type eq 'U') {
3813 $late_subtotal{$section} += $usage
3814 unless scalar(@$extra_sections);
3819 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3821 if (! $type || $type eq 'S') {
3822 $subtotal{$section} += $cust_bill_pkg->setup
3823 if $cust_bill_pkg->setup != 0;
3827 $subtotal{$section} += $cust_bill_pkg->recur
3828 if $cust_bill_pkg->recur != 0;
3831 if ($type && $type eq 'R') {
3832 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3833 if $cust_bill_pkg->recur != 0;
3836 if ($type && $type eq 'U') {
3837 $subtotal{$section} += $usage
3838 unless scalar(@$extra_sections);
3847 %pkg_category_cache = ();
3849 push @$late, map { { 'description' => &{$escape}($_),
3850 'subtotal' => $late_subtotal{$_},
3852 'sort_weight' => ( _pkg_category($_)
3853 ? _pkg_category($_)->weight
3856 ((_pkg_category($_) && _pkg_category($_)->condense)
3857 ? $self->_condense_section($format)
3861 sort _sectionsort keys %late_subtotal;
3864 if ( $summarypage ) {
3865 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3866 map { $_->categoryname } qsearch('pkg_category', {});
3867 push @sections, '' if exists($subtotal{''});
3869 @sections = keys %subtotal;
3872 my @early = map { { 'description' => &{$escape}($_),
3873 'subtotal' => $subtotal{$_},
3874 'summarized' => $not_tax{$_} ? '' : 'Y',
3875 'tax_section' => $not_tax{$_} ? '' : 'Y',
3876 'sort_weight' => ( _pkg_category($_)
3877 ? _pkg_category($_)->weight
3880 ((_pkg_category($_) && _pkg_category($_)->condense)
3881 ? $self->_condense_section($format)
3886 push @early, @$extra_sections if $extra_sections;
3888 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3892 #helper subs for above
3895 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3899 my $categoryname = shift;
3900 $pkg_category_cache{$categoryname} ||=
3901 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3904 my %condensed_format = (
3905 'label' => [ qw( Description Qty Amount ) ],
3907 sub { shift->{description} },
3908 sub { shift->{quantity} },
3909 sub { my($href, %opt) = @_;
3910 ($opt{dollar} || ''). $href->{amount};
3913 'align' => [ qw( l r r ) ],
3914 'span' => [ qw( 5 1 1 ) ], # unitprices?
3915 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3918 sub _condense_section {
3919 my ( $self, $format ) = ( shift, shift );
3921 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3922 qw( description_generator
3925 total_line_generator
3930 sub _condensed_generator_defaults {
3931 my ( $self, $format ) = ( shift, shift );
3932 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3941 sub _condensed_header_generator {
3942 my ( $self, $format ) = ( shift, shift );
3944 my ( $f, $prefix, $suffix, $separator, $column ) =
3945 _condensed_generator_defaults($format);
3947 if ($format eq 'latex') {
3948 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3949 $suffix = "\\\\\n\\hline";
3952 sub { my ($d,$a,$s,$w) = @_;
3953 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3955 } elsif ( $format eq 'html' ) {
3956 $prefix = '<th></th>';
3960 sub { my ($d,$a,$s,$w) = @_;
3961 return qq!<th align="$html_align{$a}">$d</th>!;
3969 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3971 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3974 $prefix. join($separator, @result). $suffix;
3979 sub _condensed_description_generator {
3980 my ( $self, $format ) = ( shift, shift );
3982 my ( $f, $prefix, $suffix, $separator, $column ) =
3983 _condensed_generator_defaults($format);
3985 my $money_char = '$';
3986 if ($format eq 'latex') {
3987 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3989 $separator = " & \n";
3991 sub { my ($d,$a,$s,$w) = @_;
3992 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3994 $money_char = '\\dollar';
3995 }elsif ( $format eq 'html' ) {
3996 $prefix = '"><td align="center"></td>';
4000 sub { my ($d,$a,$s,$w) = @_;
4001 return qq!<td align="$html_align{$a}">$d</td>!;
4003 #$money_char = $conf->config('money_char') || '$';
4004 $money_char = ''; # this is madness
4012 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4014 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4016 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4017 map { $f->{$_}->[$i] } qw(align span width)
4021 $prefix. join( $separator, @result ). $suffix;
4026 sub _condensed_total_generator {
4027 my ( $self, $format ) = ( shift, shift );
4029 my ( $f, $prefix, $suffix, $separator, $column ) =
4030 _condensed_generator_defaults($format);
4033 if ($format eq 'latex') {
4036 $separator = " & \n";
4038 sub { my ($d,$a,$s,$w) = @_;
4039 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4041 }elsif ( $format eq 'html' ) {
4045 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4047 sub { my ($d,$a,$s,$w) = @_;
4048 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4057 # my $r = &{$f->{fields}->[$i]}(@args);
4058 # $r .= ' Total' unless $i;
4060 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4062 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4063 map { $f->{$_}->[$i] } qw(align span width)
4067 $prefix. join( $separator, @result ). $suffix;
4072 =item total_line_generator FORMAT
4074 Returns a coderef used for generation of invoice total line items for this
4075 usage_class. FORMAT is either html or latex
4079 # should not be used: will have issues with hash element names (description vs
4080 # total_item and amount vs total_amount -- another array of functions?
4082 sub _condensed_total_line_generator {
4083 my ( $self, $format ) = ( shift, shift );
4085 my ( $f, $prefix, $suffix, $separator, $column ) =
4086 _condensed_generator_defaults($format);
4089 if ($format eq 'latex') {
4092 $separator = " & \n";
4094 sub { my ($d,$a,$s,$w) = @_;
4095 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4097 }elsif ( $format eq 'html' ) {
4101 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4103 sub { my ($d,$a,$s,$w) = @_;
4104 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4113 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4115 &{$column}( &{$f->{fields}->[$i]}(@args),
4116 map { $f->{$_}->[$i] } qw(align span width)
4120 $prefix. join( $separator, @result ). $suffix;
4125 #sub _items_extra_usage_sections {
4127 # my $escape = shift;
4129 # my %sections = ();
4131 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4132 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4134 # next unless $cust_bill_pkg->pkgnum > 0;
4136 # foreach my $section ( keys %usage_class ) {
4138 # my $usage = $cust_bill_pkg->usage($section);
4140 # next unless $usage && $usage > 0;
4142 # $sections{$section} ||= 0;
4143 # $sections{$section} += $usage;
4149 # map { { 'description' => &{$escape}($_),
4150 # 'subtotal' => $sections{$_},
4151 # 'summarized' => '',
4152 # 'tax_section' => '',
4155 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4159 sub _items_extra_usage_sections {
4161 my $conf = $self->conf;
4169 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4171 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4172 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4173 next unless $cust_bill_pkg->pkgnum > 0;
4175 foreach my $classnum ( keys %usage_class ) {
4176 my $section = $usage_class{$classnum}->classname;
4177 $classnums{$section} = $classnum;
4179 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4180 my $amount = $detail->amount;
4181 next unless $amount && $amount > 0;
4183 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4184 $sections{$section}{amount} += $amount; #subtotal
4185 $sections{$section}{calls}++;
4186 $sections{$section}{duration} += $detail->duration;
4188 my $desc = $detail->regionname;
4189 my $description = $desc;
4190 $description = substr($desc, 0, $maxlength). '...'
4191 if $format eq 'latex' && length($desc) > $maxlength;
4193 $lines{$section}{$desc} ||= {
4194 description => &{$escape}($description),
4195 #pkgpart => $part_pkg->pkgpart,
4196 pkgnum => $cust_bill_pkg->pkgnum,
4201 #unit_amount => $cust_bill_pkg->unitrecur,
4202 quantity => $cust_bill_pkg->quantity,
4203 product_code => 'N/A',
4204 ext_description => [],
4207 $lines{$section}{$desc}{amount} += $amount;
4208 $lines{$section}{$desc}{calls}++;
4209 $lines{$section}{$desc}{duration} += $detail->duration;
4215 my %sectionmap = ();
4216 foreach (keys %sections) {
4217 my $usage_class = $usage_class{$classnums{$_}};
4218 $sectionmap{$_} = { 'description' => &{$escape}($_),
4219 'amount' => $sections{$_}{amount}, #subtotal
4220 'calls' => $sections{$_}{calls},
4221 'duration' => $sections{$_}{duration},
4223 'tax_section' => '',
4224 'sort_weight' => $usage_class->weight,
4225 ( $usage_class->format
4226 ? ( map { $_ => $usage_class->$_($format) }
4227 qw( description_generator header_generator total_generator total_line_generator )
4234 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4238 foreach my $section ( keys %lines ) {
4239 foreach my $line ( keys %{$lines{$section}} ) {
4240 my $l = $lines{$section}{$line};
4241 $l->{section} = $sectionmap{$section};
4242 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4243 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4248 return(\@sections, \@lines);
4254 my $end = $self->_date;
4256 # start at date of previous invoice + 1 second or 0 if no previous invoice
4257 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4258 $start = 0 if !$start;
4261 my $cust_main = $self->cust_main;
4262 my @pkgs = $cust_main->all_pkgs;
4263 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4266 foreach my $pkg ( @pkgs ) {
4267 my @h_cust_svc = $pkg->h_cust_svc($end);
4268 foreach my $h_cust_svc ( @h_cust_svc ) {
4269 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4270 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4272 my $inserted = $h_cust_svc->date_inserted;
4273 my $deleted = $h_cust_svc->date_deleted;
4274 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4276 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4278 # DID either activated or ported in; cannot be both for same DID simultaneously
4279 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4280 && (!$phone_inserted->lnp_status
4281 || $phone_inserted->lnp_status eq ''
4282 || $phone_inserted->lnp_status eq 'native')) {
4285 else { # this one not so clean, should probably move to (h_)svc_phone
4286 my $phone_portedin = qsearchs( 'h_svc_phone',
4287 { 'svcnum' => $h_cust_svc->svcnum,
4288 'lnp_status' => 'portedin' },
4289 FS::h_svc_phone->sql_h_searchs($end),
4291 $num_portedin++ if $phone_portedin;
4294 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4295 if($deleted >= $start && $deleted <= $end && $phone_deleted
4296 && (!$phone_deleted->lnp_status
4297 || $phone_deleted->lnp_status ne 'portingout')) {
4300 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4301 && $phone_deleted->lnp_status
4302 && $phone_deleted->lnp_status eq 'portingout') {
4306 # increment usage minutes
4307 if ( $phone_inserted ) {
4308 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4309 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4312 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4315 # don't look at this service again
4316 push @seen, $h_cust_svc->svcnum;
4320 $minutes = sprintf("%d", $minutes);
4321 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4322 . "$num_deactivated Ported-Out: $num_portedout ",
4323 "Total Minutes: $minutes");
4326 sub _items_accountcode_cdr {
4331 my $section = { 'amount' => 0,
4334 'sort_weight' => '',
4336 'description' => 'Usage by Account Code',
4342 my %accountcodes = ();
4344 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4345 next unless $cust_bill_pkg->pkgnum > 0;
4347 my @header = $cust_bill_pkg->details_header;
4348 next unless scalar(@header);
4349 $section->{'header'} = join(',',@header);
4351 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4353 $section->{'header'} = $detail->formatted('format' => $format)
4354 if($detail->detail eq $section->{'header'});
4356 my $accountcode = $detail->accountcode;
4357 next unless $accountcode;
4359 my $amount = $detail->amount;
4360 next unless $amount && $amount > 0;
4362 $accountcodes{$accountcode} ||= {
4363 description => $accountcode,
4370 product_code => 'N/A',
4371 section => $section,
4372 ext_description => [ $section->{'header'} ],
4376 $section->{'amount'} += $amount;
4377 $accountcodes{$accountcode}{'amount'} += $amount;
4378 $accountcodes{$accountcode}{calls}++;
4379 $accountcodes{$accountcode}{duration} += $detail->duration;
4380 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4384 foreach my $l ( values %accountcodes ) {
4385 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4386 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4387 foreach my $sorted_detail ( @sorted_detail ) {
4388 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4390 delete $l->{detail_temp};
4394 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4396 return ($section,\@sorted_lines);
4399 sub _items_svc_phone_sections {
4401 my $conf = $self->conf;
4409 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4411 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4412 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4414 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4415 next unless $cust_bill_pkg->pkgnum > 0;
4417 my @header = $cust_bill_pkg->details_header;
4418 next unless scalar(@header);
4420 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4422 my $phonenum = $detail->phonenum;
4423 next unless $phonenum;
4425 my $amount = $detail->amount;
4426 next unless $amount && $amount > 0;
4428 $sections{$phonenum} ||= { 'amount' => 0,
4431 'sort_weight' => -1,
4432 'phonenum' => $phonenum,
4434 $sections{$phonenum}{amount} += $amount; #subtotal
4435 $sections{$phonenum}{calls}++;
4436 $sections{$phonenum}{duration} += $detail->duration;
4438 my $desc = $detail->regionname;
4439 my $description = $desc;
4440 $description = substr($desc, 0, $maxlength). '...'
4441 if $format eq 'latex' && length($desc) > $maxlength;
4443 $lines{$phonenum}{$desc} ||= {
4444 description => &{$escape}($description),
4445 #pkgpart => $part_pkg->pkgpart,
4453 product_code => 'N/A',
4454 ext_description => [],
4457 $lines{$phonenum}{$desc}{amount} += $amount;
4458 $lines{$phonenum}{$desc}{calls}++;
4459 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4461 my $line = $usage_class{$detail->classnum}->classname;
4462 $sections{"$phonenum $line"} ||=
4466 'sort_weight' => $usage_class{$detail->classnum}->weight,
4467 'phonenum' => $phonenum,
4468 'header' => [ @header ],
4470 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4471 $sections{"$phonenum $line"}{calls}++;
4472 $sections{"$phonenum $line"}{duration} += $detail->duration;
4474 $lines{"$phonenum $line"}{$desc} ||= {
4475 description => &{$escape}($description),
4476 #pkgpart => $part_pkg->pkgpart,
4484 product_code => 'N/A',
4485 ext_description => [],
4488 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4489 $lines{"$phonenum $line"}{$desc}{calls}++;
4490 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4491 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4492 $detail->formatted('format' => $format);
4497 my %sectionmap = ();
4498 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4499 foreach ( keys %sections ) {
4500 my @header = @{ $sections{$_}{header} || [] };
4502 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4503 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4504 my $usage_class = $summary ? $simple : $usage_simple;
4505 my $ending = $summary ? ' usage charges' : '';
4508 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4510 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4511 'amount' => $sections{$_}{amount}, #subtotal
4512 'calls' => $sections{$_}{calls},
4513 'duration' => $sections{$_}{duration},
4515 'tax_section' => '',
4516 'phonenum' => $sections{$_}{phonenum},
4517 'sort_weight' => $sections{$_}{sort_weight},
4518 'post_total' => $summary, #inspire pagebreak
4520 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4521 qw( description_generator
4524 total_line_generator
4531 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4532 $a->{sort_weight} <=> $b->{sort_weight}
4537 foreach my $section ( keys %lines ) {
4538 foreach my $line ( keys %{$lines{$section}} ) {
4539 my $l = $lines{$section}{$line};
4540 $l->{section} = $sectionmap{$section};
4541 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4542 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4547 if($conf->exists('phone_usage_class_summary')) {
4548 # this only works with Latex
4552 # after this, we'll have only two sections per DID:
4553 # Calls Summary and Calls Detail
4554 foreach my $section ( @sections ) {
4555 if($section->{'post_total'}) {
4556 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4557 $section->{'total_line_generator'} = sub { '' };
4558 $section->{'total_generator'} = sub { '' };
4559 $section->{'header_generator'} = sub { '' };
4560 $section->{'description_generator'} = '';
4561 push @newsections, $section;
4562 my %calls_detail = %$section;
4563 $calls_detail{'post_total'} = '';
4564 $calls_detail{'sort_weight'} = '';
4565 $calls_detail{'description_generator'} = sub { '' };
4566 $calls_detail{'header_generator'} = sub {
4567 return ' & Date/Time & Called Number & Duration & Price'
4568 if $format eq 'latex';
4571 $calls_detail{'description'} = 'Calls Detail: '
4572 . $section->{'phonenum'};
4573 push @newsections, \%calls_detail;
4577 # after this, each usage class is collapsed/summarized into a single
4578 # line under the Calls Summary section
4579 foreach my $newsection ( @newsections ) {
4580 if($newsection->{'post_total'}) { # this means Calls Summary
4581 foreach my $section ( @sections ) {
4582 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4583 && !$section->{'post_total'});
4584 my $newdesc = $section->{'description'};
4585 my $tn = $section->{'phonenum'};
4586 $newdesc =~ s/$tn//g;
4587 my $line = { ext_description => [],
4591 calls => $section->{'calls'},
4592 section => $newsection,
4593 duration => $section->{'duration'},
4594 description => $newdesc,
4595 amount => sprintf("%.2f",$section->{'amount'}),
4596 product_code => 'N/A',
4598 push @newlines, $line;
4603 # after this, Calls Details is populated with all CDRs
4604 foreach my $newsection ( @newsections ) {
4605 if(!$newsection->{'post_total'}) { # this means Calls Details
4606 foreach my $line ( @lines ) {
4607 next unless (scalar(@{$line->{'ext_description'}}) &&
4608 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4610 my @extdesc = @{$line->{'ext_description'}};
4612 foreach my $extdesc ( @extdesc ) {
4613 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4614 push @newextdesc, $extdesc;
4616 $line->{'ext_description'} = \@newextdesc;
4617 $line->{'section'} = $newsection;
4618 push @newlines, $line;
4623 return(\@newsections, \@newlines);
4626 return(\@sections, \@lines);
4630 sub _items { # seems to be unused
4633 #my @display = scalar(@_)
4635 # : qw( _items_previous _items_pkg );
4636 # #: qw( _items_pkg );
4637 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4638 my @display = qw( _items_previous _items_pkg );
4641 foreach my $display ( @display ) {
4642 push @b, $self->$display(@_);
4647 sub _items_previous {
4649 my $conf = $self->conf;
4650 my $cust_main = $self->cust_main;
4651 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4653 foreach ( @pr_cust_bill ) {
4654 my $date = $conf->exists('invoice_show_prior_due_date')
4655 ? 'due '. $_->due_date2str($date_format)
4656 : time2str($date_format, $_->_date);
4658 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4659 #'pkgpart' => 'N/A',
4661 'amount' => sprintf("%.2f", $_->owed),
4667 # 'description' => 'Previous Balance',
4668 # #'pkgpart' => 'N/A',
4669 # 'pkgnum' => 'N/A',
4670 # 'amount' => sprintf("%10.2f", $pr_total ),
4671 # 'ext_description' => [ map {
4672 # "Invoice ". $_->invnum.
4673 # " (". time2str("%x",$_->_date). ") ".
4674 # sprintf("%10.2f", $_->owed)
4675 # } @pr_cust_bill ],
4680 =item _items_pkg [ OPTIONS ]
4682 Return line item hashes for each package item on this invoice. Nearly
4685 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4687 The only OPTIONS accepted is 'section', which may point to a hashref
4688 with a key named 'condensed', which may have a true value. If it
4689 does, this method tries to merge identical items into items with
4690 'quantity' equal to the number of items (not the sum of their
4691 separate quantities, for some reason).
4699 warn "$me _items_pkg searching for all package line items\n"
4702 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4704 warn "$me _items_pkg filtering line items\n"
4706 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4708 if ($options{section} && $options{section}->{condensed}) {
4710 warn "$me _items_pkg condensing section\n"
4714 local $Storable::canonical = 1;
4715 foreach ( @items ) {
4717 delete $item->{ref};
4718 delete $item->{ext_description};
4719 my $key = freeze($item);
4720 $itemshash{$key} ||= 0;
4721 $itemshash{$key} ++; # += $item->{quantity};
4723 @items = sort { $a->{description} cmp $b->{description} }
4724 map { my $i = thaw($_);
4725 $i->{quantity} = $itemshash{$_};
4727 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4733 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4740 return 0 unless $a->itemdesc cmp $b->itemdesc;
4741 return -1 if $b->itemdesc eq 'Tax';
4742 return 1 if $a->itemdesc eq 'Tax';
4743 return -1 if $b->itemdesc eq 'Other surcharges';
4744 return 1 if $a->itemdesc eq 'Other surcharges';
4745 $a->itemdesc cmp $b->itemdesc;
4750 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4751 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4754 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4756 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4757 list of hashrefs describing the line items they generate on the invoice.
4759 OPTIONS may include:
4761 format: the invoice format.
4763 escape_function: the function used to escape strings.
4765 format_function: the function used to format CDRs.
4767 section: a hashref containing 'description'; if this is present,
4768 cust_bill_pkg_display records not belonging to this section are
4771 multisection: a flag indicating that this is a multisection invoice,
4772 which does something complicated.
4774 multilocation: a flag to display the location label for the package.
4776 Returns a list of hashrefs, each of which may contain:
4778 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4779 ext_description, which is an arrayref of detail lines to show below
4784 sub _items_cust_bill_pkg {
4786 my $conf = $self->conf;
4787 my $cust_bill_pkgs = shift;
4790 my $format = $opt{format} || '';
4791 my $escape_function = $opt{escape_function} || sub { shift };
4792 my $format_function = $opt{format_function} || '';
4793 my $unsquelched = $opt{unsquelched} || ''; #unused
4794 my $section = $opt{section}->{description} if $opt{section};
4795 my $summary_page = $opt{summary_page} || ''; #unused
4796 my $multilocation = $opt{multilocation} || '';
4797 my $multisection = $opt{multisection} || '';
4798 my $discount_show_always = 0;
4800 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4803 my ($s, $r, $u) = ( undef, undef, undef );
4804 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4807 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4808 if ( $_ && !$cust_bill_pkg->hidden ) {
4809 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4810 $_->{amount} =~ s/^\-0\.00$/0.00/;
4811 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4813 if $_->{amount} != 0
4814 || $discount_show_always
4815 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4816 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4822 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4823 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4826 foreach my $display ( grep { defined($section)
4827 ? $_->section eq $section
4830 #grep { !$_->summary || !$summary_page } # bunk!
4831 grep { !$_->summary || $multisection }
4832 $cust_bill_pkg->cust_bill_pkg_display
4836 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4837 $display->billpkgdisplaynum. "\n"
4840 my $type = $display->type;
4842 my $desc = $cust_bill_pkg->desc;
4843 $desc = substr($desc, 0, $maxlength). '...'
4844 if $format eq 'latex' && length($desc) > $maxlength;
4846 my %details_opt = ( 'format' => $format,
4847 'escape_function' => $escape_function,
4848 'format_function' => $format_function,
4851 if ( $cust_bill_pkg->pkgnum > 0 ) {
4853 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4856 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4858 # start/end dates for invoice formats that do nonstandard
4860 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4862 if ( (!$type || $type eq 'S')
4863 && ( $cust_bill_pkg->setup != 0
4864 || $cust_bill_pkg->setup_show_zero
4869 warn "$me _items_cust_bill_pkg adding setup\n"
4872 my $description = $desc;
4873 $description .= ' Setup'
4874 if $cust_bill_pkg->recur != 0
4875 || $discount_show_always
4876 || $cust_bill_pkg->recur_show_zero;
4879 unless ( $cust_pkg->part_pkg->hide_svc_detail
4880 || $cust_bill_pkg->hidden )
4883 push @d, map &{$escape_function}($_),
4884 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4885 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4887 if ( $multilocation ) {
4888 my $loc = $cust_pkg->location_label;
4889 $loc = substr($loc, 0, $maxlength). '...'
4890 if $format eq 'latex' && length($loc) > $maxlength;
4891 push @d, &{$escape_function}($loc);
4894 } #unless hiding service details
4896 push @d, $cust_bill_pkg->details(%details_opt)
4897 if $cust_bill_pkg->recur == 0;
4899 if ( $cust_bill_pkg->hidden ) {
4900 $s->{amount} += $cust_bill_pkg->setup;
4901 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4902 push @{ $s->{ext_description} }, @d;
4906 description => $description,
4907 #pkgpart => $part_pkg->pkgpart,
4908 pkgnum => $cust_bill_pkg->pkgnum,
4909 amount => $cust_bill_pkg->setup,
4910 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4911 unit_amount => $cust_bill_pkg->unitsetup,
4912 quantity => $cust_bill_pkg->quantity,
4913 ext_description => \@d,
4919 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4921 $cust_bill_pkg->recur != 0
4922 || $cust_bill_pkg->setup == 0
4923 || $discount_show_always
4924 || $cust_bill_pkg->recur_show_zero
4929 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4932 my $is_summary = $display->summary;
4933 my $description = ($is_summary && $type && $type eq 'U')
4934 ? "Usage charges" : $desc;
4936 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4937 " - ". time2str($date_format, $cust_bill_pkg->edate).
4939 unless $conf->exists('disable_line_item_date_ranges')
4940 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4943 my @seconds = (); # for display of usage info
4945 #at least until cust_bill_pkg has "past" ranges in addition to
4946 #the "future" sdate/edate ones... see #3032
4947 my @dates = ( $self->_date );
4948 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4949 push @dates, $prev->sdate if $prev;
4950 push @dates, undef if !$prev;
4952 unless ( $cust_pkg->part_pkg->hide_svc_detail
4953 || $cust_bill_pkg->itemdesc
4954 || $cust_bill_pkg->hidden
4955 || $is_summary && $type && $type eq 'U' )
4958 warn "$me _items_cust_bill_pkg adding service details\n"
4961 push @d, map &{$escape_function}($_),
4962 $cust_pkg->h_labels_short(@dates, 'I')
4963 #$cust_bill_pkg->edate,
4964 #$cust_bill_pkg->sdate)
4965 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4967 warn "$me _items_cust_bill_pkg done adding service details\n"
4970 if ( $multilocation ) {
4971 my $loc = $cust_pkg->location_label;
4972 $loc = substr($loc, 0, $maxlength). '...'
4973 if $format eq 'latex' && length($loc) > $maxlength;
4974 push @d, &{$escape_function}($loc);
4977 # Display of seconds_since_sqlradacct:
4978 # On the invoice, when processing @detail_items, look for a field
4979 # named 'seconds'. This will contain total seconds for each
4980 # service, in the same order as @ext_description. For services
4981 # that don't support this it will show undef.
4982 if ( $conf->exists('svc_acct-usage_seconds')
4983 and ! $cust_bill_pkg->pkgpart_override ) {
4984 foreach my $cust_svc (
4985 $cust_pkg->h_cust_svc(@dates, 'I')
4988 # eval because not having any part_export_usage exports
4989 # is a fatal error, last_bill/_date because that's how
4990 # sqlradius_hour billing does it
4992 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4994 push @seconds, $sec;
4996 } #if svc_acct-usage_seconds
5000 unless ( $is_summary ) {
5001 warn "$me _items_cust_bill_pkg adding details\n"
5004 #instead of omitting details entirely in this case (unwanted side
5005 # effects), just omit CDRs
5006 $details_opt{'format_function'} = sub { () }
5007 if $type && $type eq 'R';
5009 push @d, $cust_bill_pkg->details(%details_opt);
5012 warn "$me _items_cust_bill_pkg calculating amount\n"
5017 $amount = $cust_bill_pkg->recur;
5018 } elsif ($type eq 'R') {
5019 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5020 } elsif ($type eq 'U') {
5021 $amount = $cust_bill_pkg->usage;
5024 if ( !$type || $type eq 'R' ) {
5026 warn "$me _items_cust_bill_pkg adding recur\n"
5029 if ( $cust_bill_pkg->hidden ) {
5030 $r->{amount} += $amount;
5031 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5032 push @{ $r->{ext_description} }, @d;
5035 description => $description,
5036 #pkgpart => $part_pkg->pkgpart,
5037 pkgnum => $cust_bill_pkg->pkgnum,
5039 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5040 unit_amount => $cust_bill_pkg->unitrecur,
5041 quantity => $cust_bill_pkg->quantity,
5043 ext_description => \@d,
5045 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5048 } else { # $type eq 'U'
5050 warn "$me _items_cust_bill_pkg adding usage\n"
5053 if ( $cust_bill_pkg->hidden ) {
5054 $u->{amount} += $amount;
5055 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5056 push @{ $u->{ext_description} }, @d;
5059 description => $description,
5060 #pkgpart => $part_pkg->pkgpart,
5061 pkgnum => $cust_bill_pkg->pkgnum,
5063 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5064 unit_amount => $cust_bill_pkg->unitrecur,
5065 quantity => $cust_bill_pkg->quantity,
5067 ext_description => \@d,
5072 } # recurring or usage with recurring charge
5074 } else { #pkgnum tax or one-shot line item (??)
5076 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5079 if ( $cust_bill_pkg->setup != 0 ) {
5081 'description' => $desc,
5082 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5085 if ( $cust_bill_pkg->recur != 0 ) {
5087 'description' => "$desc (".
5088 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5089 time2str($date_format, $cust_bill_pkg->edate). ')',
5090 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5098 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5099 && $conf->exists('discount-show-always'));
5103 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5105 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5106 $_->{amount} =~ s/^\-0\.00$/0.00/;
5107 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5109 if $_->{amount} != 0
5110 || $discount_show_always
5111 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5112 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5116 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5123 sub _items_credits {
5124 my( $self, %opt ) = @_;
5125 my $trim_len = $opt{'trim_len'} || 60;
5129 foreach ( $self->cust_credited ) {
5131 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5133 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5134 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5135 $reason = " ($reason) " if $reason;
5138 #'description' => 'Credit ref\#'. $_->crednum.
5139 # " (". time2str("%x",$_->cust_credit->_date) .")".
5141 'description' => $self->mt('Credit applied').' '.
5142 time2str($date_format,$_->cust_credit->_date). $reason,
5143 'amount' => sprintf("%.2f",$_->amount),
5151 sub _items_payments {
5155 #get & print payments
5156 foreach ( $self->cust_bill_pay ) {
5158 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5161 'description' => $self->mt('Payment received').' '.
5162 time2str($date_format,$_->cust_pay->_date ),
5163 'amount' => sprintf("%.2f", $_->amount )
5171 =item _items_discounts_avail
5173 Returns an array of line item hashrefs representing available term discounts
5174 for this invoice. This makes the same assumptions that apply to term
5175 discounts in general: that the package is billed monthly, at a flat rate,
5176 with no usage charges. A prorated first month will be handled, as will
5177 a setup fee if the discount is allowed to apply to setup fees.
5181 sub _items_discounts_avail {
5184 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5186 my ($previous_balance) = $self->previous;
5188 foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5189 $terms{$_->months} = {
5191 base => $previous_balance || 0, # pre-discount sum of charges
5192 discounted => $previous_balance || 0, # post-discount sum
5193 list_pkgnums => 0, # whether any packages are not discounted
5196 foreach my $months (keys %terms) {
5197 my $hash = $terms{$months};
5199 # tricky, because packages may not all be eligible for the same discounts
5200 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5201 my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5202 my $part_pkg = $cust_pkg->part_pkg or next;
5204 next if $part_pkg->freq ne '1';
5205 my $setup = $cust_bill_pkg->setup || 0;
5206 my $recur = $cust_bill_pkg->recur || 0;
5207 my $permonth = $part_pkg->base_recur_permonth || 0;
5209 my ($discount) = grep { $_->months == $months }
5210 map { $_->discount } $part_pkg->part_pkg_discount;
5212 $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5216 if ( $discount->setup ) {
5217 $discountable += $setup;
5220 $hash->{discounted} += $setup;
5223 if ( $discount->percent ) {
5224 $discountable += $months * $permonth;
5225 $discountable -= ($discountable * $discount->percent / 100);
5226 $discountable -= ($permonth - $recur); # correct for prorate
5227 $hash->{discounted} += $discountable;
5230 $discountable += $recur;
5231 $discountable -= $discount->amount * $recur/$permonth;
5233 $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5236 $hash->{discounted} += $discountable;
5237 push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5240 $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5241 $hash->{list_pkgnums} = 1;
5243 } #foreach $cust_bill_pkg
5245 # don't show this line if no packages have discounts at this term
5246 # or if there are no new charges to apply the discount to
5247 delete $terms{$months} if $hash->{base} == $hash->{discounted}
5248 or $hash->{base} == 0;
5252 $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5254 foreach my $months (keys %terms) {
5255 my $hash = $terms{$months};
5256 my $term_total = sprintf('%.2f', $hash->{discounted});
5257 # possibly shouldn't include previous balance in these?
5258 my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5259 my $permonth = sprintf('%.2f', $term_total / $months);
5261 $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5264 $hash->{amount} = $self->mt('[_1] ([_2] per month)',
5265 $term_total, $money_char.$permonth
5269 if ( $list_pkgnums ) {
5270 push @detail, $self->mt('discount on item'). ' '.
5271 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5273 $hash->{ext_description} = join ', ', @detail;
5276 map { $terms{$_} } sort {$b <=> $a} keys %terms;
5279 =item call_details [ OPTION => VALUE ... ]
5281 Returns an array of CSV strings representing the call details for this invoice
5282 The only option available is the boolean prepend_billed_number
5287 my ($self, %opt) = @_;
5289 my $format_function = sub { shift };
5291 if ($opt{prepend_billed_number}) {
5292 $format_function = sub {
5296 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5301 my @details = map { $_->details( 'format_function' => $format_function,
5302 'escape_function' => sub{ return() },
5306 $self->cust_bill_pkg;
5307 my $header = $details[0];
5308 ( $header, grep { $_ ne $header } @details );
5318 =item process_reprint
5322 sub process_reprint {
5323 process_re_X('print', @_);
5326 =item process_reemail
5330 sub process_reemail {
5331 process_re_X('email', @_);
5339 process_re_X('fax', @_);
5347 process_re_X('ftp', @_);
5354 sub process_respool {
5355 process_re_X('spool', @_);
5358 use Storable qw(thaw);
5362 my( $method, $job ) = ( shift, shift );
5363 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5365 my $param = thaw(decode_base64(shift));
5366 warn Dumper($param) if $DEBUG;
5377 my($method, $job, %param ) = @_;
5379 warn "re_X $method for job $job with param:\n".
5380 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5383 #some false laziness w/search/cust_bill.html
5385 my $orderby = 'ORDER BY cust_bill._date';
5387 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5389 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5391 my @cust_bill = qsearch( {
5392 #'select' => "cust_bill.*",
5393 'table' => 'cust_bill',
5394 'addl_from' => $addl_from,
5396 'extra_sql' => $extra_sql,
5397 'order_by' => $orderby,
5401 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5403 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5406 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5407 foreach my $cust_bill ( @cust_bill ) {
5408 $cust_bill->$method();
5410 if ( $job ) { #progressbar foo
5412 if ( time - $min_sec > $last ) {
5413 my $error = $job->update_statustext(
5414 int( 100 * $num / scalar(@cust_bill) )
5416 die $error if $error;
5427 =head1 CLASS METHODS
5433 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5438 my ($class, $start, $end) = @_;
5440 $class->paid_sql($start, $end). ' - '.
5441 $class->credited_sql($start, $end);
5446 Returns an SQL fragment to retreive the net amount (charged minus credited).
5451 my ($class, $start, $end) = @_;
5452 'charged - '. $class->credited_sql($start, $end);
5457 Returns an SQL fragment to retreive the amount paid against this invoice.
5462 my ($class, $start, $end) = @_;
5463 $start &&= "AND cust_bill_pay._date <= $start";
5464 $end &&= "AND cust_bill_pay._date > $end";
5465 $start = '' unless defined($start);
5466 $end = '' unless defined($end);
5467 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5468 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5473 Returns an SQL fragment to retreive the amount credited against this invoice.
5478 my ($class, $start, $end) = @_;
5479 $start &&= "AND cust_credit_bill._date <= $start";
5480 $end &&= "AND cust_credit_bill._date > $end";
5481 $start = '' unless defined($start);
5482 $end = '' unless defined($end);
5483 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5484 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5489 Returns an SQL fragment to retrieve the due date of an invoice.
5490 Currently only supported on PostgreSQL.
5495 my $conf = new FS::Conf;
5499 cust_bill.invoice_terms,
5500 cust_main.invoice_terms,
5501 \''.($conf->config('invoice_default_terms') || '').'\'
5502 ), E\'Net (\\\\d+)\'
5504 ) * 86400 + cust_bill._date'
5507 =item search_sql_where HASHREF
5509 Class method which returns an SQL WHERE fragment to search for parameters
5510 specified in HASHREF. Valid parameters are
5516 List reference of start date, end date, as UNIX timestamps.
5526 List reference of charged limits (exclusive).
5530 List reference of charged limits (exclusive).
5534 flag, return open invoices only
5538 flag, return net invoices only
5542 =item newest_percust
5546 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5550 sub search_sql_where {
5551 my($class, $param) = @_;
5553 warn "$me search_sql_where called with params: \n".
5554 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5560 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5561 push @search, "cust_main.agentnum = $1";
5565 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5566 push @search, "cust_bill.custnum = $1";
5570 if ( $param->{_date} ) {
5571 my($beginning, $ending) = @{$param->{_date}};
5573 push @search, "cust_bill._date >= $beginning",
5574 "cust_bill._date < $ending";
5578 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5579 push @search, "cust_bill.invnum >= $1";
5581 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5582 push @search, "cust_bill.invnum <= $1";
5586 if ( $param->{charged} ) {
5587 my @charged = ref($param->{charged})
5588 ? @{ $param->{charged} }
5589 : ($param->{charged});
5591 push @search, map { s/^charged/cust_bill.charged/; $_; }
5595 my $owed_sql = FS::cust_bill->owed_sql;
5598 if ( $param->{owed} ) {
5599 my @owed = ref($param->{owed})
5600 ? @{ $param->{owed} }
5602 push @search, map { s/^owed/$owed_sql/; $_; }
5607 push @search, "0 != $owed_sql"
5608 if $param->{'open'};
5609 push @search, '0 != '. FS::cust_bill->net_sql
5613 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5614 if $param->{'days'};
5617 if ( $param->{'newest_percust'} ) {
5619 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5620 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5622 my @newest_where = map { my $x = $_;
5623 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5626 grep ! /^cust_main./, @search;
5627 my $newest_where = scalar(@newest_where)
5628 ? ' AND '. join(' AND ', @newest_where)
5632 push @search, "cust_bill._date = (
5633 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5634 WHERE newest_cust_bill.custnum = cust_bill.custnum
5640 #agent virtualization
5641 my $curuser = $FS::CurrentUser::CurrentUser;
5642 if ( $curuser->username eq 'fs_queue'
5643 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5645 my $newuser = qsearchs('access_user', {
5646 'username' => $username,
5650 $curuser = $newuser;
5652 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5655 push @search, $curuser->agentnums_sql;
5657 join(' AND ', @search );
5669 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5670 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base