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)
2367 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2368 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2369 # yes: fixed width (dot matrix) text printing will be borked
2371 my( $self, %params ) = @_;
2372 my $conf = $self->conf;
2373 my $today = $params{today} ? $params{today} : time;
2374 warn "$me print_generic called on $self with suffix $params{template}\n"
2377 my $format = $params{format};
2378 die "Unknown format: $format"
2379 unless $format =~ /^(latex|html|template)$/;
2381 my $cust_main = $self->cust_main;
2382 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2383 unless $cust_main->payname
2384 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2386 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2387 'html' => [ '<%=', '%>' ],
2388 'template' => [ '{', '}' ],
2391 warn "$me print_generic creating template\n"
2394 #create the template
2395 my $template = $params{template} ? $params{template} : $self->_agent_template;
2396 my $templatefile = "invoice_$format";
2397 $templatefile .= "_$template"
2398 if length($template) && $conf->exists($templatefile."_$template");
2399 my @invoice_template = map "$_\n", $conf->config($templatefile)
2400 or die "cannot load config data $templatefile";
2403 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2404 #change this to a die when the old code is removed
2405 warn "old-style invoice template $templatefile; ".
2406 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2407 $old_latex = 'true';
2408 @invoice_template = _translate_old_latex_format(@invoice_template);
2411 warn "$me print_generic creating T:T object\n"
2414 my $text_template = new Text::Template(
2416 SOURCE => \@invoice_template,
2417 DELIMITERS => $delimiters{$format},
2420 warn "$me print_generic compiling T:T object\n"
2423 $text_template->compile()
2424 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2427 # additional substitution could possibly cause breakage in existing templates
2428 my %convert_maps = (
2430 'notes' => sub { map "$_", @_ },
2431 'footer' => sub { map "$_", @_ },
2432 'smallfooter' => sub { map "$_", @_ },
2433 'returnaddress' => sub { map "$_", @_ },
2434 'coupon' => sub { map "$_", @_ },
2435 'summary' => sub { map "$_", @_ },
2441 s/%%(.*)$/<!-- $1 -->/g;
2442 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2443 s/\\begin\{enumerate\}/<ol>/g;
2445 s/\\end\{enumerate\}/<\/ol>/g;
2446 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2455 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2457 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2462 s/\\\\\*?\s*$/<BR>/;
2463 s/\\hyphenation\{[\w\s\-]+}//;
2468 'coupon' => sub { "" },
2469 'summary' => sub { "" },
2476 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2477 s/\\begin\{enumerate\}//g;
2479 s/\\end\{enumerate\}//g;
2480 s/\\textbf\{(.*)\}/$1/g;
2487 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2489 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2494 s/\\\\\*?\s*$/\n/; # dubious
2495 s/\\hyphenation\{[\w\s\-]+}//;
2499 'coupon' => sub { "" },
2500 'summary' => sub { "" },
2505 # hashes for differing output formats
2506 my %nbsps = ( 'latex' => '~',
2507 'html' => '', # '&nbps;' would be nice
2508 'template' => '', # not used
2510 my $nbsp = $nbsps{$format};
2512 my %escape_functions = ( 'latex' => \&_latex_escape,
2513 'html' => \&_html_escape_nbsp,#\&encode_entities,
2514 'template' => sub { shift },
2516 my $escape_function = $escape_functions{$format};
2517 my $escape_function_nonbsp = ($format eq 'html')
2518 ? \&_html_escape : $escape_function;
2520 my %date_formats = ( 'latex' => $date_format_long,
2521 'html' => $date_format_long,
2524 $date_formats{'html'} =~ s/ / /g;
2526 my $date_format = $date_formats{$format};
2528 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2530 'html' => sub { return '<b>'. shift(). '</b>'
2532 'template' => sub { shift },
2534 my $embolden_function = $embolden_functions{$format};
2536 my %newline_tokens = ( 'latex' => '\\\\',
2540 my $newline_token = $newline_tokens{$format};
2542 warn "$me generating template variables\n"
2545 # generate template variables
2548 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2552 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2558 $returnaddress = join("\n",
2559 $conf->config_orbase("invoice_${format}returnaddress", $template)
2562 } elsif ( grep /\S/,
2563 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2565 my $convert_map = $convert_maps{$format}{'returnaddress'};
2568 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2573 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2575 my $convert_map = $convert_maps{$format}{'returnaddress'};
2576 $returnaddress = join( "\n", &$convert_map(
2577 map { s/( {2,})/'~' x length($1)/eg;
2581 ( $conf->config('company_name', $self->cust_main->agentnum),
2582 $conf->config('company_address', $self->cust_main->agentnum),
2589 my $warning = "Couldn't find a return address; ".
2590 "do you need to set the company_address configuration value?";
2592 $returnaddress = $nbsp;
2593 #$returnaddress = $warning;
2597 warn "$me generating invoice data\n"
2600 my $agentnum = $self->cust_main->agentnum;
2602 my %invoice_data = (
2605 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2606 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2607 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2608 'returnaddress' => $returnaddress,
2609 'agent' => &$escape_function($cust_main->agent->agent),
2612 'invnum' => $self->invnum,
2613 'date' => time2str($date_format, $self->_date),
2614 'today' => time2str($date_format_long, $today),
2615 'terms' => $self->terms,
2616 'template' => $template, #params{'template'},
2617 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2618 'current_charges' => sprintf("%.2f", $self->charged),
2619 'duedate' => $self->due_date2str($rdate_format), #date_format?
2622 'custnum' => $cust_main->display_custnum,
2623 'agent_custid' => &$escape_function($cust_main->agent_custid),
2624 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2625 payname company address1 address2 city state zip fax
2629 'ship_enable' => $conf->exists('invoice-ship_address'),
2630 'unitprices' => $conf->exists('invoice-unitprice'),
2631 'smallernotes' => $conf->exists('invoice-smallernotes'),
2632 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2633 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2635 #layout info -- would be fancy to calc some of this and bury the template
2637 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2638 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2639 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2640 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2641 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2642 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2643 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2644 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2645 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2646 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2648 # better hang on to conf_dir for a while (for old templates)
2649 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2651 #these are only used when doing paged plaintext
2658 my $lh = FS::L10N->get_handle($cust_main->locale);
2659 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2660 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2661 # eval to avoid death for unimplemented languages
2662 my $dh = eval { Date::Language->new($info{'name'}) } ||
2663 Date::Language->new(); # fall back to English
2664 # prototype here to silence warnings
2665 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2666 # eventually use this date handle everywhere in here, too
2668 my $min_sdate = 999999999999;
2670 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2671 next unless $cust_bill_pkg->pkgnum > 0;
2672 $min_sdate = $cust_bill_pkg->sdate
2673 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2674 $max_edate = $cust_bill_pkg->edate
2675 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2678 $invoice_data{'bill_period'} = '';
2679 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2680 . " to " . time2str('%e %h', $max_edate)
2681 if ($max_edate != 0 && $min_sdate != 999999999999);
2683 $invoice_data{finance_section} = '';
2684 if ( $conf->config('finance_pkgclass') ) {
2686 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2687 $invoice_data{finance_section} = $pkg_class->categoryname;
2689 $invoice_data{finance_amount} = '0.00';
2690 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2692 my $countrydefault = $conf->config('countrydefault') || 'US';
2693 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2694 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2695 my $method = $prefix.$_;
2696 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2698 $invoice_data{'ship_country'} = ''
2699 if ( $invoice_data{'ship_country'} eq $countrydefault );
2701 $invoice_data{'cid'} = $params{'cid'}
2704 if ( $cust_main->country eq $countrydefault ) {
2705 $invoice_data{'country'} = '';
2707 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2711 $invoice_data{'address'} = \@address;
2713 $cust_main->payname.
2714 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2715 ? " (P.O. #". $cust_main->payinfo. ")"
2719 push @address, $cust_main->company
2720 if $cust_main->company;
2721 push @address, $cust_main->address1;
2722 push @address, $cust_main->address2
2723 if $cust_main->address2;
2725 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2726 push @address, $invoice_data{'country'}
2727 if $invoice_data{'country'};
2729 while (scalar(@address) < 5);
2731 $invoice_data{'logo_file'} = $params{'logo_file'}
2732 if $params{'logo_file'};
2733 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2734 if $params{'barcode_file'};
2735 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2736 if $params{'barcode_img'};
2737 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2738 if $params{'barcode_cid'};
2740 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2741 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2742 #my $balance_due = $self->owed + $pr_total - $cr_total;
2743 my $balance_due = $self->owed + $pr_total;
2745 # the customer's current balance as shown on the invoice before this one
2746 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2748 # the change in balance from that invoice to this one
2749 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2751 # the sum of amount owed on all previous invoices
2752 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2754 # the sum of amount owed on all invoices
2755 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2757 # info from customer's last invoice before this one, for some
2759 $invoice_data{'last_bill'} = {};
2760 my $last_bill = $pr_cust_bill[-1];
2762 $invoice_data{'last_bill'} = {
2763 '_date' => $last_bill->_date, #unformatted
2764 # all we need for now
2768 my $summarypage = '';
2769 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2772 $invoice_data{'summarypage'} = $summarypage;
2774 warn "$me substituting variables in notes, footer, smallfooter\n"
2777 my @include = (qw( notes footer smallfooter ));
2778 push @include, 'coupon' unless $params{'no_coupon'};
2779 foreach my $include (@include) {
2781 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2784 if ( $conf->exists($inc_file, $agentnum)
2785 && length( $conf->config($inc_file, $agentnum) ) ) {
2787 @inc_src = $conf->config($inc_file, $agentnum);
2791 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2793 my $convert_map = $convert_maps{$format}{$include};
2795 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2796 s/--\@\]/$delimiters{$format}[1]/g;
2799 &$convert_map( $conf->config($inc_file, $agentnum) );
2803 my $inc_tt = new Text::Template (
2805 SOURCE => [ map "$_\n", @inc_src ],
2806 DELIMITERS => $delimiters{$format},
2807 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2809 unless ( $inc_tt->compile() ) {
2810 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2811 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2815 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2817 $invoice_data{$include} =~ s/\n+$//
2818 if ($format eq 'latex');
2821 # let invoices use either of these as needed
2822 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2823 ? $cust_main->payinfo : '';
2824 $invoice_data{'po_line'} =
2825 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2826 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2829 my %money_chars = ( 'latex' => '',
2830 'html' => $conf->config('money_char') || '$',
2833 my $money_char = $money_chars{$format};
2835 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2836 'html' => $conf->config('money_char') || '$',
2839 my $other_money_char = $other_money_chars{$format};
2840 $invoice_data{'dollar'} = $other_money_char;
2842 my @detail_items = ();
2843 my @total_items = ();
2847 $invoice_data{'detail_items'} = \@detail_items;
2848 $invoice_data{'total_items'} = \@total_items;
2849 $invoice_data{'buf'} = \@buf;
2850 $invoice_data{'sections'} = \@sections;
2852 warn "$me generating sections\n"
2855 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2856 'subtotal' => $other_money_char.
2857 sprintf('%.2f', $pr_total),
2858 'summarized' => '', #why? $summarypage ? 'Y' : '',
2860 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2861 join(' / ', map { $cust_main->balance_date_range(@$_) }
2862 $self->_prior_month30s
2864 if $conf->exists('invoice_include_aging');
2867 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2868 'subtotal' => $taxtotal, # adjusted below
2870 my $tax_weight = _pkg_category($tax_section->{description})
2871 ? _pkg_category($tax_section->{description})->weight
2873 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2874 $tax_section->{'sort_weight'} = $tax_weight;
2877 my $adjusttotal = 0;
2878 my $adjust_section = { 'description' =>
2879 $self->mt('Credits, Payments, and Adjustments'),
2880 'subtotal' => 0, # adjusted below
2882 my $adjust_weight = _pkg_category($adjust_section->{description})
2883 ? _pkg_category($adjust_section->{description})->weight
2885 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2886 $adjust_section->{'sort_weight'} = $adjust_weight;
2888 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2889 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2890 $invoice_data{'multisection'} = $multisection;
2891 my $late_sections = [];
2892 my $extra_sections = [];
2893 my $extra_lines = ();
2894 if ( $multisection ) {
2895 ($extra_sections, $extra_lines) =
2896 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2897 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2899 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2901 push @detail_items, @$extra_lines if $extra_lines;
2903 $self->_items_sections( $late_sections, # this could stand a refactor
2905 $escape_function_nonbsp,
2909 if ($conf->exists('svc_phone_sections')) {
2910 my ($phone_sections, $phone_lines) =
2911 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2912 push @{$late_sections}, @$phone_sections;
2913 push @detail_items, @$phone_lines;
2915 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2916 my ($accountcode_section, $accountcode_lines) =
2917 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2918 if ( scalar(@$accountcode_lines) ) {
2919 push @{$late_sections}, $accountcode_section;
2920 push @detail_items, @$accountcode_lines;
2923 } else {# not multisection
2924 # make a default section
2925 push @sections, { 'description' => '', 'subtotal' => '',
2926 'no_subtotal' => 1 };
2927 # and calculate the finance charge total, since it won't get done otherwise.
2928 # XXX possibly other totals?
2929 # XXX possibly finance_pkgclass should not be used in this manner?
2930 if ( $conf->exists('finance_pkgclass') ) {
2931 my @finance_charges;
2932 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2933 if ( grep { $_->section eq $invoice_data{finance_section} }
2934 $cust_bill_pkg->cust_bill_pkg_display ) {
2935 # I think these are always setup fees, but just to be sure...
2936 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2939 $invoice_data{finance_amount} =
2940 sprintf('%.2f', sum( @finance_charges ) || 0);
2944 unless ( $conf->exists('disable_previous_balance')
2945 || $conf->exists('previous_balance-summary_only')
2949 warn "$me adding previous balances\n"
2952 foreach my $line_item ( $self->_items_previous ) {
2955 ext_description => [],
2957 $detail->{'ref'} = $line_item->{'pkgnum'};
2958 $detail->{'quantity'} = 1;
2959 $detail->{'section'} = $previous_section;
2960 $detail->{'description'} = &$escape_function($line_item->{'description'});
2961 if ( exists $line_item->{'ext_description'} ) {
2962 @{$detail->{'ext_description'}} = map {
2963 &$escape_function($_);
2964 } @{$line_item->{'ext_description'}};
2966 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2967 $line_item->{'amount'};
2968 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2970 push @detail_items, $detail;
2971 push @buf, [ $detail->{'description'},
2972 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2978 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2979 push @buf, ['','-----------'];
2980 push @buf, [ $self->mt('Total Previous Balance'),
2981 $money_char. sprintf("%10.2f", $pr_total) ];
2985 if ( $conf->exists('svc_phone-did-summary') ) {
2986 warn "$me adding DID summary\n"
2989 my ($didsummary,$minutes) = $self->_did_summary;
2990 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2992 { 'description' => $didsummary_desc,
2993 'ext_description' => [ $didsummary, $minutes ],
2997 foreach my $section (@sections, @$late_sections) {
2999 warn "$me adding section \n". Dumper($section)
3002 # begin some normalization
3003 $section->{'subtotal'} = $section->{'amount'}
3005 && !exists($section->{subtotal})
3006 && exists($section->{amount});
3008 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3009 if ( $invoice_data{finance_section} &&
3010 $section->{'description'} eq $invoice_data{finance_section} );
3012 $section->{'subtotal'} = $other_money_char.
3013 sprintf('%.2f', $section->{'subtotal'})
3016 # continue some normalization
3017 $section->{'amount'} = $section->{'subtotal'}
3021 if ( $section->{'description'} ) {
3022 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3027 warn "$me setting options\n"
3030 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3032 $options{'section'} = $section if $multisection;
3033 $options{'format'} = $format;
3034 $options{'escape_function'} = $escape_function;
3035 $options{'format_function'} = sub { () } unless $unsquelched;
3036 $options{'unsquelched'} = $unsquelched;
3037 $options{'summary_page'} = $summarypage;
3038 $options{'skip_usage'} =
3039 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3040 $options{'multilocation'} = $multilocation;
3041 $options{'multisection'} = $multisection;
3043 warn "$me searching for line items\n"
3046 foreach my $line_item ( $self->_items_pkg(%options) ) {
3048 warn "$me adding line item $line_item\n"
3052 ext_description => [],
3054 $detail->{'ref'} = $line_item->{'pkgnum'};
3055 $detail->{'quantity'} = $line_item->{'quantity'};
3056 $detail->{'section'} = $section;
3057 $detail->{'description'} = &$escape_function($line_item->{'description'});
3058 if ( exists $line_item->{'ext_description'} ) {
3059 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3061 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3062 $line_item->{'amount'};
3063 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3064 $line_item->{'unit_amount'};
3065 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3067 $detail->{'sdate'} = $line_item->{'sdate'};
3068 $detail->{'edate'} = $line_item->{'edate'};
3069 $detail->{'seconds'} = $line_item->{'seconds'};
3071 push @detail_items, $detail;
3072 push @buf, ( [ $detail->{'description'},
3073 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3075 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3079 if ( $section->{'description'} ) {
3080 push @buf, ( ['','-----------'],
3081 [ $section->{'description'}. ' sub-total',
3082 $section->{'subtotal'} # already formatted this
3091 $invoice_data{current_less_finance} =
3092 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3094 if ( $multisection && !$conf->exists('disable_previous_balance')
3095 || $conf->exists('previous_balance-summary_only') )
3097 unshift @sections, $previous_section if $pr_total;
3100 warn "$me adding taxes\n"
3103 foreach my $tax ( $self->_items_tax ) {
3105 $taxtotal += $tax->{'amount'};
3107 my $description = &$escape_function( $tax->{'description'} );
3108 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3110 if ( $multisection ) {
3112 my $money = $old_latex ? '' : $money_char;
3113 push @detail_items, {
3114 ext_description => [],
3117 description => $description,
3118 amount => $money. $amount,
3120 section => $tax_section,
3125 push @total_items, {
3126 'total_item' => $description,
3127 'total_amount' => $other_money_char. $amount,
3132 push @buf,[ $description,
3133 $money_char. $amount,
3140 $total->{'total_item'} = $self->mt('Sub-total');
3141 $total->{'total_amount'} =
3142 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3144 if ( $multisection ) {
3145 $tax_section->{'subtotal'} = $other_money_char.
3146 sprintf('%.2f', $taxtotal);
3147 $tax_section->{'pretotal'} = 'New charges sub-total '.
3148 $total->{'total_amount'};
3149 push @sections, $tax_section if $taxtotal;
3151 unshift @total_items, $total;
3154 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3156 push @buf,['','-----------'];
3157 push @buf,[$self->mt(
3158 $conf->exists('disable_previous_balance')
3160 : 'Total New Charges'
3162 $money_char. sprintf("%10.2f",$self->charged) ];
3168 $item = $conf->config('previous_balance-exclude_from_total')
3169 || 'Total New Charges'
3170 if $conf->exists('previous_balance-exclude_from_total');
3171 my $amount = $self->charged +
3172 ( $conf->exists('disable_previous_balance') ||
3173 $conf->exists('previous_balance-exclude_from_total')
3177 $total->{'total_item'} = &$embolden_function($self->mt($item));
3178 $total->{'total_amount'} =
3179 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3180 if ( $multisection ) {
3181 if ( $adjust_section->{'sort_weight'} ) {
3182 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3183 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3185 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3186 $other_money_char. sprintf('%.2f', $self->charged );
3189 push @total_items, $total;
3191 push @buf,['','-----------'];
3194 sprintf( '%10.2f', $amount )
3199 unless ( $conf->exists('disable_previous_balance') ) {
3200 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3203 my $credittotal = 0;
3204 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3207 $total->{'total_item'} = &$escape_function($credit->{'description'});
3208 $credittotal += $credit->{'amount'};
3209 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3210 $adjusttotal += $credit->{'amount'};
3211 if ( $multisection ) {
3212 my $money = $old_latex ? '' : $money_char;
3213 push @detail_items, {
3214 ext_description => [],
3217 description => &$escape_function($credit->{'description'}),
3218 amount => $money. $credit->{'amount'},
3220 section => $adjust_section,
3223 push @total_items, $total;
3227 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3230 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3231 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3235 my $paymenttotal = 0;
3236 foreach my $payment ( $self->_items_payments ) {
3238 $total->{'total_item'} = &$escape_function($payment->{'description'});
3239 $paymenttotal += $payment->{'amount'};
3240 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3241 $adjusttotal += $payment->{'amount'};
3242 if ( $multisection ) {
3243 my $money = $old_latex ? '' : $money_char;
3244 push @detail_items, {
3245 ext_description => [],
3248 description => &$escape_function($payment->{'description'}),
3249 amount => $money. $payment->{'amount'},
3251 section => $adjust_section,
3254 push @total_items, $total;
3256 push @buf, [ $payment->{'description'},
3257 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3260 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3262 if ( $multisection ) {
3263 $adjust_section->{'subtotal'} = $other_money_char.
3264 sprintf('%.2f', $adjusttotal);
3265 push @sections, $adjust_section
3266 unless $adjust_section->{sort_weight};
3269 # create Balance Due message
3272 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3273 $total->{'total_amount'} =
3274 &$embolden_function(
3275 $other_money_char. sprintf('%.2f', $summarypage
3277 $self->billing_balance
3278 : $self->owed + $pr_total
3281 if ( $multisection && !$adjust_section->{sort_weight} ) {
3282 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3283 $total->{'total_amount'};
3285 push @total_items, $total;
3287 push @buf,['','-----------'];
3288 push @buf,[$self->balance_due_msg, $money_char.
3289 sprintf("%10.2f", $balance_due ) ];
3292 if ( $conf->exists('previous_balance-show_credit')
3293 and $cust_main->balance < 0 ) {
3294 my $credit_total = {
3295 'total_item' => &$embolden_function($self->credit_balance_msg),
3296 'total_amount' => &$embolden_function(
3297 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3300 if ( $multisection ) {
3301 $adjust_section->{'posttotal'} .= $newline_token .
3302 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3305 push @total_items, $credit_total;
3307 push @buf,['','-----------'];
3308 push @buf,[$self->credit_balance_msg, $money_char.
3309 sprintf("%10.2f", -$cust_main->balance ) ];
3313 if ( $multisection ) {
3314 if ($conf->exists('svc_phone_sections')) {
3316 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3317 $total->{'total_amount'} =
3318 &$embolden_function(
3319 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3321 my $last_section = pop @sections;
3322 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3323 $total->{'total_amount'};
3324 push @sections, $last_section;
3326 push @sections, @$late_sections
3330 # make a discounts-available section, even without multisection
3331 if ( $conf->exists('discount-show_available')
3332 and my @discounts_avail = $self->_items_discounts_avail ) {
3333 my $discount_section = {
3334 'description' => $self->mt('Discounts Available'),
3339 push @sections, $discount_section;
3340 push @detail_items, map { +{
3341 'ref' => '', #should this be something else?
3342 'section' => $discount_section,
3343 'description' => &$escape_function( $_->{description} ),
3344 'amount' => $money_char . &$escape_function( $_->{amount} ),
3345 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3346 } } @discounts_avail;
3349 # All sections and items are built; now fill in templates.
3350 my @includelist = ();
3351 push @includelist, 'summary' if $summarypage;
3352 foreach my $include ( @includelist ) {
3354 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3357 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3359 @inc_src = $conf->config($inc_file, $agentnum);
3363 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3365 my $convert_map = $convert_maps{$format}{$include};
3367 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3368 s/--\@\]/$delimiters{$format}[1]/g;
3371 &$convert_map( $conf->config($inc_file, $agentnum) );
3375 my $inc_tt = new Text::Template (
3377 SOURCE => [ map "$_\n", @inc_src ],
3378 DELIMITERS => $delimiters{$format},
3379 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3381 unless ( $inc_tt->compile() ) {
3382 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3383 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3387 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3389 $invoice_data{$include} =~ s/\n+$//
3390 if ($format eq 'latex');
3395 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3396 /invoice_lines\((\d*)\)/;
3397 $invoice_lines += $1 || scalar(@buf);
3400 die "no invoice_lines() functions in template?"
3401 if ( $format eq 'template' && !$wasfunc );
3403 if ($format eq 'template') {
3405 if ( $invoice_lines ) {
3406 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3407 $invoice_data{'total_pages'}++
3408 if scalar(@buf) % $invoice_lines;
3411 #setup subroutine for the template
3412 $invoice_data{invoice_lines} = sub {
3413 my $lines = shift || scalar(@buf);
3425 push @collect, split("\n",
3426 $text_template->fill_in( HASH => \%invoice_data )
3428 $invoice_data{'page'}++;
3430 map "$_\n", @collect;
3432 # this is where we actually create the invoice
3433 warn "filling in template for invoice ". $self->invnum. "\n"
3435 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3438 $text_template->fill_in(HASH => \%invoice_data);
3442 # helper routine for generating date ranges
3443 sub _prior_month30s {
3446 [ 1, 2592000 ], # 0-30 days ago
3447 [ 2592000, 5184000 ], # 30-60 days ago
3448 [ 5184000, 7776000 ], # 60-90 days ago
3449 [ 7776000, 0 ], # 90+ days ago
3452 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3453 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3458 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3460 Returns an postscript invoice, as a scalar.
3462 Options can be passed as a hashref (recommended) or as a list of time, template
3463 and then any key/value pairs for any other options.
3465 I<time> an optional value used to control the printing of overdue messages. The
3466 default is now. It isn't the date of the invoice; that's the `_date' field.
3467 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3468 L<Time::Local> and L<Date::Parse> for conversion functions.
3470 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3477 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3478 my $ps = generate_ps($file);
3480 unlink($barcodefile) if $barcodefile;
3485 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3487 Returns an PDF invoice, as a scalar.
3489 Options can be passed as a hashref (recommended) or as a list of time, template
3490 and then any key/value pairs for any other options.
3492 I<time> an optional value used to control the printing of overdue messages. The
3493 default is now. It isn't the date of the invoice; that's the `_date' field.
3494 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3495 L<Time::Local> and L<Date::Parse> for conversion functions.
3497 I<template>, if specified, is the name of a suffix for alternate invoices.
3499 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3506 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3507 my $pdf = generate_pdf($file);
3509 unlink($barcodefile) if $barcodefile;
3514 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3516 Returns an HTML invoice, as a scalar.
3518 I<time> an optional value used to control the printing of overdue messages. The
3519 default is now. It isn't the date of the invoice; that's the `_date' field.
3520 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3521 L<Time::Local> and L<Date::Parse> for conversion functions.
3523 I<template>, if specified, is the name of a suffix for alternate invoices.
3525 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3527 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3528 when emailing the invoice as part of a multipart/related MIME email.
3536 %params = %{ shift() };
3538 $params{'time'} = shift;
3539 $params{'template'} = shift;
3540 $params{'cid'} = shift;
3543 $params{'format'} = 'html';
3545 $self->print_generic( %params );
3548 # quick subroutine for print_latex
3550 # There are ten characters that LaTeX treats as special characters, which
3551 # means that they do not simply typeset themselves:
3552 # # $ % & ~ _ ^ \ { }
3554 # TeX ignores blanks following an escaped character; if you want a blank (as
3555 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3559 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3560 $value =~ s/([<>])/\$$1\$/g;
3566 encode_entities($value);
3570 sub _html_escape_nbsp {
3571 my $value = _html_escape(shift);
3572 $value =~ s/ +/ /g;
3576 #utility methods for print_*
3578 sub _translate_old_latex_format {
3579 warn "_translate_old_latex_format called\n"
3586 if ( $line =~ /^%%Detail\s*$/ ) {
3588 push @template, q![@--!,
3589 q! foreach my $_tr_line (@detail_items) {!,
3590 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3591 q! $_tr_line->{'description'} .= !,
3592 q! "\\tabularnewline\n~~".!,
3593 q! join( "\\tabularnewline\n~~",!,
3594 q! @{$_tr_line->{'ext_description'}}!,
3598 while ( ( my $line_item_line = shift )
3599 !~ /^%%EndDetail\s*$/ ) {
3600 $line_item_line =~ s/'/\\'/g; # nice LTS
3601 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3602 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3603 push @template, " \$OUT .= '$line_item_line';";
3606 push @template, '}',
3609 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3611 push @template, '[@--',
3612 ' foreach my $_tr_line (@total_items) {';
3614 while ( ( my $total_item_line = shift )
3615 !~ /^%%EndTotalDetails\s*$/ ) {
3616 $total_item_line =~ s/'/\\'/g; # nice LTS
3617 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3618 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3619 push @template, " \$OUT .= '$total_item_line';";
3622 push @template, '}',
3626 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3627 push @template, $line;
3633 warn "$_\n" foreach @template;
3641 my $conf = $self->conf;
3643 #check for an invoice-specific override
3644 return $self->invoice_terms if $self->invoice_terms;
3646 #check for a customer- specific override
3647 my $cust_main = $self->cust_main;
3648 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3650 #use configured default
3651 $conf->config('invoice_default_terms') || '';
3657 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3658 $duedate = $self->_date() + ( $1 * 86400 );
3665 $self->due_date ? time2str(shift, $self->due_date) : '';
3668 sub balance_due_msg {
3670 my $msg = $self->mt('Balance Due');
3671 return $msg unless $self->terms;
3672 if ( $self->due_date ) {
3673 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3674 $self->due_date2str($date_format);
3675 } elsif ( $self->terms ) {
3676 $msg .= ' - '. $self->terms;
3681 sub balance_due_date {
3683 my $conf = $self->conf;
3685 if ( $conf->exists('invoice_default_terms')
3686 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3687 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3692 sub credit_balance_msg {
3694 $self->mt('Credit Balance Remaining')
3697 =item invnum_date_pretty
3699 Returns a string with the invoice number and date, for example:
3700 "Invoice #54 (3/20/2008)"
3704 sub invnum_date_pretty {
3706 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3711 Returns a string with the date, for example: "3/20/2008"
3717 time2str($date_format, $self->_date);
3720 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3722 Generate section information for all items appearing on this invoice.
3723 This will only be called for multi-section invoices.
3725 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3726 related display records (L<FS::cust_bill_pkg_display>) and organize
3727 them into two groups ("early" and "late" according to whether they come
3728 before or after the total), then into sections. A subtotal is calculated
3731 Section descriptions are returned in sort weight order. Each consists
3732 of a hash containing:
3734 description: the package category name, escaped
3735 subtotal: the total charges in that section
3736 tax_section: a flag indicating that the section contains only tax charges
3737 summarized: same as tax_section, for some reason
3738 sort_weight: the package category's sort weight
3740 If 'condense' is set on the display record, it also contains everything
3741 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3742 coderefs to generate parts of the invoice. This is not advised.
3746 LATE: an arrayref to push the "late" section hashes onto. The "early"
3747 group is simply returned from the method.
3749 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3750 Turning this on has the following effects:
3751 - Ignores display items with the 'summary' flag.
3752 - Combines all items into the "early" group.
3753 - Creates sections for all non-disabled package categories, even if they
3754 have no charges on this invoice, as well as a section with no name.
3756 ESCAPE: an escape function to use for section titles.
3758 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3759 sorted list. If there are any of these, section subtotals exclude
3762 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3763 passed through to C<_condense_section()>.
3767 use vars qw(%pkg_category_cache);
3768 sub _items_sections {
3771 my $summarypage = shift;
3773 my $extra_sections = shift;
3777 my %late_subtotal = ();
3780 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3783 my $usage = $cust_bill_pkg->usage;
3785 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3786 next if ( $display->summary && $summarypage );
3788 my $section = $display->section;
3789 my $type = $display->type;
3791 $not_tax{$section} = 1
3792 unless $cust_bill_pkg->pkgnum == 0;
3794 if ( $display->post_total && !$summarypage ) {
3795 if (! $type || $type eq 'S') {
3796 $late_subtotal{$section} += $cust_bill_pkg->setup
3797 if $cust_bill_pkg->setup != 0;
3801 $late_subtotal{$section} += $cust_bill_pkg->recur
3802 if $cust_bill_pkg->recur != 0;
3805 if ($type && $type eq 'R') {
3806 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3807 if $cust_bill_pkg->recur != 0;
3810 if ($type && $type eq 'U') {
3811 $late_subtotal{$section} += $usage
3812 unless scalar(@$extra_sections);
3817 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3819 if (! $type || $type eq 'S') {
3820 $subtotal{$section} += $cust_bill_pkg->setup
3821 if $cust_bill_pkg->setup != 0;
3825 $subtotal{$section} += $cust_bill_pkg->recur
3826 if $cust_bill_pkg->recur != 0;
3829 if ($type && $type eq 'R') {
3830 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3831 if $cust_bill_pkg->recur != 0;
3834 if ($type && $type eq 'U') {
3835 $subtotal{$section} += $usage
3836 unless scalar(@$extra_sections);
3845 %pkg_category_cache = ();
3847 push @$late, map { { 'description' => &{$escape}($_),
3848 'subtotal' => $late_subtotal{$_},
3850 'sort_weight' => ( _pkg_category($_)
3851 ? _pkg_category($_)->weight
3854 ((_pkg_category($_) && _pkg_category($_)->condense)
3855 ? $self->_condense_section($format)
3859 sort _sectionsort keys %late_subtotal;
3862 if ( $summarypage ) {
3863 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3864 map { $_->categoryname } qsearch('pkg_category', {});
3865 push @sections, '' if exists($subtotal{''});
3867 @sections = keys %subtotal;
3870 my @early = map { { 'description' => &{$escape}($_),
3871 'subtotal' => $subtotal{$_},
3872 'summarized' => $not_tax{$_} ? '' : 'Y',
3873 'tax_section' => $not_tax{$_} ? '' : 'Y',
3874 'sort_weight' => ( _pkg_category($_)
3875 ? _pkg_category($_)->weight
3878 ((_pkg_category($_) && _pkg_category($_)->condense)
3879 ? $self->_condense_section($format)
3884 push @early, @$extra_sections if $extra_sections;
3886 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3890 #helper subs for above
3893 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3897 my $categoryname = shift;
3898 $pkg_category_cache{$categoryname} ||=
3899 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3902 my %condensed_format = (
3903 'label' => [ qw( Description Qty Amount ) ],
3905 sub { shift->{description} },
3906 sub { shift->{quantity} },
3907 sub { my($href, %opt) = @_;
3908 ($opt{dollar} || ''). $href->{amount};
3911 'align' => [ qw( l r r ) ],
3912 'span' => [ qw( 5 1 1 ) ], # unitprices?
3913 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3916 sub _condense_section {
3917 my ( $self, $format ) = ( shift, shift );
3919 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3920 qw( description_generator
3923 total_line_generator
3928 sub _condensed_generator_defaults {
3929 my ( $self, $format ) = ( shift, shift );
3930 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3939 sub _condensed_header_generator {
3940 my ( $self, $format ) = ( shift, shift );
3942 my ( $f, $prefix, $suffix, $separator, $column ) =
3943 _condensed_generator_defaults($format);
3945 if ($format eq 'latex') {
3946 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3947 $suffix = "\\\\\n\\hline";
3950 sub { my ($d,$a,$s,$w) = @_;
3951 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3953 } elsif ( $format eq 'html' ) {
3954 $prefix = '<th></th>';
3958 sub { my ($d,$a,$s,$w) = @_;
3959 return qq!<th align="$html_align{$a}">$d</th>!;
3967 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3969 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3972 $prefix. join($separator, @result). $suffix;
3977 sub _condensed_description_generator {
3978 my ( $self, $format ) = ( shift, shift );
3980 my ( $f, $prefix, $suffix, $separator, $column ) =
3981 _condensed_generator_defaults($format);
3983 my $money_char = '$';
3984 if ($format eq 'latex') {
3985 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3987 $separator = " & \n";
3989 sub { my ($d,$a,$s,$w) = @_;
3990 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3992 $money_char = '\\dollar';
3993 }elsif ( $format eq 'html' ) {
3994 $prefix = '"><td align="center"></td>';
3998 sub { my ($d,$a,$s,$w) = @_;
3999 return qq!<td align="$html_align{$a}">$d</td>!;
4001 #$money_char = $conf->config('money_char') || '$';
4002 $money_char = ''; # this is madness
4010 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4012 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4014 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4015 map { $f->{$_}->[$i] } qw(align span width)
4019 $prefix. join( $separator, @result ). $suffix;
4024 sub _condensed_total_generator {
4025 my ( $self, $format ) = ( shift, shift );
4027 my ( $f, $prefix, $suffix, $separator, $column ) =
4028 _condensed_generator_defaults($format);
4031 if ($format eq 'latex') {
4034 $separator = " & \n";
4036 sub { my ($d,$a,$s,$w) = @_;
4037 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4039 }elsif ( $format eq 'html' ) {
4043 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4045 sub { my ($d,$a,$s,$w) = @_;
4046 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4055 # my $r = &{$f->{fields}->[$i]}(@args);
4056 # $r .= ' Total' unless $i;
4058 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4060 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4061 map { $f->{$_}->[$i] } qw(align span width)
4065 $prefix. join( $separator, @result ). $suffix;
4070 =item total_line_generator FORMAT
4072 Returns a coderef used for generation of invoice total line items for this
4073 usage_class. FORMAT is either html or latex
4077 # should not be used: will have issues with hash element names (description vs
4078 # total_item and amount vs total_amount -- another array of functions?
4080 sub _condensed_total_line_generator {
4081 my ( $self, $format ) = ( shift, shift );
4083 my ( $f, $prefix, $suffix, $separator, $column ) =
4084 _condensed_generator_defaults($format);
4087 if ($format eq 'latex') {
4090 $separator = " & \n";
4092 sub { my ($d,$a,$s,$w) = @_;
4093 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4095 }elsif ( $format eq 'html' ) {
4099 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4101 sub { my ($d,$a,$s,$w) = @_;
4102 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4111 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4113 &{$column}( &{$f->{fields}->[$i]}(@args),
4114 map { $f->{$_}->[$i] } qw(align span width)
4118 $prefix. join( $separator, @result ). $suffix;
4123 #sub _items_extra_usage_sections {
4125 # my $escape = shift;
4127 # my %sections = ();
4129 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4130 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4132 # next unless $cust_bill_pkg->pkgnum > 0;
4134 # foreach my $section ( keys %usage_class ) {
4136 # my $usage = $cust_bill_pkg->usage($section);
4138 # next unless $usage && $usage > 0;
4140 # $sections{$section} ||= 0;
4141 # $sections{$section} += $usage;
4147 # map { { 'description' => &{$escape}($_),
4148 # 'subtotal' => $sections{$_},
4149 # 'summarized' => '',
4150 # 'tax_section' => '',
4153 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4157 sub _items_extra_usage_sections {
4159 my $conf = $self->conf;
4167 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4169 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4170 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4171 next unless $cust_bill_pkg->pkgnum > 0;
4173 foreach my $classnum ( keys %usage_class ) {
4174 my $section = $usage_class{$classnum}->classname;
4175 $classnums{$section} = $classnum;
4177 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4178 my $amount = $detail->amount;
4179 next unless $amount && $amount > 0;
4181 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4182 $sections{$section}{amount} += $amount; #subtotal
4183 $sections{$section}{calls}++;
4184 $sections{$section}{duration} += $detail->duration;
4186 my $desc = $detail->regionname;
4187 my $description = $desc;
4188 $description = substr($desc, 0, $maxlength). '...'
4189 if $format eq 'latex' && length($desc) > $maxlength;
4191 $lines{$section}{$desc} ||= {
4192 description => &{$escape}($description),
4193 #pkgpart => $part_pkg->pkgpart,
4194 pkgnum => $cust_bill_pkg->pkgnum,
4199 #unit_amount => $cust_bill_pkg->unitrecur,
4200 quantity => $cust_bill_pkg->quantity,
4201 product_code => 'N/A',
4202 ext_description => [],
4205 $lines{$section}{$desc}{amount} += $amount;
4206 $lines{$section}{$desc}{calls}++;
4207 $lines{$section}{$desc}{duration} += $detail->duration;
4213 my %sectionmap = ();
4214 foreach (keys %sections) {
4215 my $usage_class = $usage_class{$classnums{$_}};
4216 $sectionmap{$_} = { 'description' => &{$escape}($_),
4217 'amount' => $sections{$_}{amount}, #subtotal
4218 'calls' => $sections{$_}{calls},
4219 'duration' => $sections{$_}{duration},
4221 'tax_section' => '',
4222 'sort_weight' => $usage_class->weight,
4223 ( $usage_class->format
4224 ? ( map { $_ => $usage_class->$_($format) }
4225 qw( description_generator header_generator total_generator total_line_generator )
4232 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4236 foreach my $section ( keys %lines ) {
4237 foreach my $line ( keys %{$lines{$section}} ) {
4238 my $l = $lines{$section}{$line};
4239 $l->{section} = $sectionmap{$section};
4240 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4241 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4246 return(\@sections, \@lines);
4252 my $end = $self->_date;
4254 # start at date of previous invoice + 1 second or 0 if no previous invoice
4255 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4256 $start = 0 if !$start;
4259 my $cust_main = $self->cust_main;
4260 my @pkgs = $cust_main->all_pkgs;
4261 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4264 foreach my $pkg ( @pkgs ) {
4265 my @h_cust_svc = $pkg->h_cust_svc($end);
4266 foreach my $h_cust_svc ( @h_cust_svc ) {
4267 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4268 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4270 my $inserted = $h_cust_svc->date_inserted;
4271 my $deleted = $h_cust_svc->date_deleted;
4272 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4274 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4276 # DID either activated or ported in; cannot be both for same DID simultaneously
4277 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4278 && (!$phone_inserted->lnp_status
4279 || $phone_inserted->lnp_status eq ''
4280 || $phone_inserted->lnp_status eq 'native')) {
4283 else { # this one not so clean, should probably move to (h_)svc_phone
4284 my $phone_portedin = qsearchs( 'h_svc_phone',
4285 { 'svcnum' => $h_cust_svc->svcnum,
4286 'lnp_status' => 'portedin' },
4287 FS::h_svc_phone->sql_h_searchs($end),
4289 $num_portedin++ if $phone_portedin;
4292 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4293 if($deleted >= $start && $deleted <= $end && $phone_deleted
4294 && (!$phone_deleted->lnp_status
4295 || $phone_deleted->lnp_status ne 'portingout')) {
4298 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4299 && $phone_deleted->lnp_status
4300 && $phone_deleted->lnp_status eq 'portingout') {
4304 # increment usage minutes
4305 if ( $phone_inserted ) {
4306 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4307 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4310 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4313 # don't look at this service again
4314 push @seen, $h_cust_svc->svcnum;
4318 $minutes = sprintf("%d", $minutes);
4319 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4320 . "$num_deactivated Ported-Out: $num_portedout ",
4321 "Total Minutes: $minutes");
4324 sub _items_accountcode_cdr {
4329 my $section = { 'amount' => 0,
4332 'sort_weight' => '',
4334 'description' => 'Usage by Account Code',
4340 my %accountcodes = ();
4342 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4343 next unless $cust_bill_pkg->pkgnum > 0;
4345 my @header = $cust_bill_pkg->details_header;
4346 next unless scalar(@header);
4347 $section->{'header'} = join(',',@header);
4349 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4351 $section->{'header'} = $detail->formatted('format' => $format)
4352 if($detail->detail eq $section->{'header'});
4354 my $accountcode = $detail->accountcode;
4355 next unless $accountcode;
4357 my $amount = $detail->amount;
4358 next unless $amount && $amount > 0;
4360 $accountcodes{$accountcode} ||= {
4361 description => $accountcode,
4368 product_code => 'N/A',
4369 section => $section,
4370 ext_description => [ $section->{'header'} ],
4374 $section->{'amount'} += $amount;
4375 $accountcodes{$accountcode}{'amount'} += $amount;
4376 $accountcodes{$accountcode}{calls}++;
4377 $accountcodes{$accountcode}{duration} += $detail->duration;
4378 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4382 foreach my $l ( values %accountcodes ) {
4383 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4384 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4385 foreach my $sorted_detail ( @sorted_detail ) {
4386 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4388 delete $l->{detail_temp};
4392 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4394 return ($section,\@sorted_lines);
4397 sub _items_svc_phone_sections {
4399 my $conf = $self->conf;
4407 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4409 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4410 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4412 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4413 next unless $cust_bill_pkg->pkgnum > 0;
4415 my @header = $cust_bill_pkg->details_header;
4416 next unless scalar(@header);
4418 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4420 my $phonenum = $detail->phonenum;
4421 next unless $phonenum;
4423 my $amount = $detail->amount;
4424 next unless $amount && $amount > 0;
4426 $sections{$phonenum} ||= { 'amount' => 0,
4429 'sort_weight' => -1,
4430 'phonenum' => $phonenum,
4432 $sections{$phonenum}{amount} += $amount; #subtotal
4433 $sections{$phonenum}{calls}++;
4434 $sections{$phonenum}{duration} += $detail->duration;
4436 my $desc = $detail->regionname;
4437 my $description = $desc;
4438 $description = substr($desc, 0, $maxlength). '...'
4439 if $format eq 'latex' && length($desc) > $maxlength;
4441 $lines{$phonenum}{$desc} ||= {
4442 description => &{$escape}($description),
4443 #pkgpart => $part_pkg->pkgpart,
4451 product_code => 'N/A',
4452 ext_description => [],
4455 $lines{$phonenum}{$desc}{amount} += $amount;
4456 $lines{$phonenum}{$desc}{calls}++;
4457 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4459 my $line = $usage_class{$detail->classnum}->classname;
4460 $sections{"$phonenum $line"} ||=
4464 'sort_weight' => $usage_class{$detail->classnum}->weight,
4465 'phonenum' => $phonenum,
4466 'header' => [ @header ],
4468 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4469 $sections{"$phonenum $line"}{calls}++;
4470 $sections{"$phonenum $line"}{duration} += $detail->duration;
4472 $lines{"$phonenum $line"}{$desc} ||= {
4473 description => &{$escape}($description),
4474 #pkgpart => $part_pkg->pkgpart,
4482 product_code => 'N/A',
4483 ext_description => [],
4486 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4487 $lines{"$phonenum $line"}{$desc}{calls}++;
4488 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4489 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4490 $detail->formatted('format' => $format);
4495 my %sectionmap = ();
4496 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4497 foreach ( keys %sections ) {
4498 my @header = @{ $sections{$_}{header} || [] };
4500 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4501 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4502 my $usage_class = $summary ? $simple : $usage_simple;
4503 my $ending = $summary ? ' usage charges' : '';
4506 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4508 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4509 'amount' => $sections{$_}{amount}, #subtotal
4510 'calls' => $sections{$_}{calls},
4511 'duration' => $sections{$_}{duration},
4513 'tax_section' => '',
4514 'phonenum' => $sections{$_}{phonenum},
4515 'sort_weight' => $sections{$_}{sort_weight},
4516 'post_total' => $summary, #inspire pagebreak
4518 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4519 qw( description_generator
4522 total_line_generator
4529 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4530 $a->{sort_weight} <=> $b->{sort_weight}
4535 foreach my $section ( keys %lines ) {
4536 foreach my $line ( keys %{$lines{$section}} ) {
4537 my $l = $lines{$section}{$line};
4538 $l->{section} = $sectionmap{$section};
4539 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4540 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4545 if($conf->exists('phone_usage_class_summary')) {
4546 # this only works with Latex
4550 # after this, we'll have only two sections per DID:
4551 # Calls Summary and Calls Detail
4552 foreach my $section ( @sections ) {
4553 if($section->{'post_total'}) {
4554 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4555 $section->{'total_line_generator'} = sub { '' };
4556 $section->{'total_generator'} = sub { '' };
4557 $section->{'header_generator'} = sub { '' };
4558 $section->{'description_generator'} = '';
4559 push @newsections, $section;
4560 my %calls_detail = %$section;
4561 $calls_detail{'post_total'} = '';
4562 $calls_detail{'sort_weight'} = '';
4563 $calls_detail{'description_generator'} = sub { '' };
4564 $calls_detail{'header_generator'} = sub {
4565 return ' & Date/Time & Called Number & Duration & Price'
4566 if $format eq 'latex';
4569 $calls_detail{'description'} = 'Calls Detail: '
4570 . $section->{'phonenum'};
4571 push @newsections, \%calls_detail;
4575 # after this, each usage class is collapsed/summarized into a single
4576 # line under the Calls Summary section
4577 foreach my $newsection ( @newsections ) {
4578 if($newsection->{'post_total'}) { # this means Calls Summary
4579 foreach my $section ( @sections ) {
4580 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4581 && !$section->{'post_total'});
4582 my $newdesc = $section->{'description'};
4583 my $tn = $section->{'phonenum'};
4584 $newdesc =~ s/$tn//g;
4585 my $line = { ext_description => [],
4589 calls => $section->{'calls'},
4590 section => $newsection,
4591 duration => $section->{'duration'},
4592 description => $newdesc,
4593 amount => sprintf("%.2f",$section->{'amount'}),
4594 product_code => 'N/A',
4596 push @newlines, $line;
4601 # after this, Calls Details is populated with all CDRs
4602 foreach my $newsection ( @newsections ) {
4603 if(!$newsection->{'post_total'}) { # this means Calls Details
4604 foreach my $line ( @lines ) {
4605 next unless (scalar(@{$line->{'ext_description'}}) &&
4606 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4608 my @extdesc = @{$line->{'ext_description'}};
4610 foreach my $extdesc ( @extdesc ) {
4611 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4612 push @newextdesc, $extdesc;
4614 $line->{'ext_description'} = \@newextdesc;
4615 $line->{'section'} = $newsection;
4616 push @newlines, $line;
4621 return(\@newsections, \@newlines);
4624 return(\@sections, \@lines);
4628 sub _items { # seems to be unused
4631 #my @display = scalar(@_)
4633 # : qw( _items_previous _items_pkg );
4634 # #: qw( _items_pkg );
4635 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4636 my @display = qw( _items_previous _items_pkg );
4639 foreach my $display ( @display ) {
4640 push @b, $self->$display(@_);
4645 sub _items_previous {
4647 my $conf = $self->conf;
4648 my $cust_main = $self->cust_main;
4649 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4651 foreach ( @pr_cust_bill ) {
4652 my $date = $conf->exists('invoice_show_prior_due_date')
4653 ? 'due '. $_->due_date2str($date_format)
4654 : time2str($date_format, $_->_date);
4656 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4657 #'pkgpart' => 'N/A',
4659 'amount' => sprintf("%.2f", $_->owed),
4665 # 'description' => 'Previous Balance',
4666 # #'pkgpart' => 'N/A',
4667 # 'pkgnum' => 'N/A',
4668 # 'amount' => sprintf("%10.2f", $pr_total ),
4669 # 'ext_description' => [ map {
4670 # "Invoice ". $_->invnum.
4671 # " (". time2str("%x",$_->_date). ") ".
4672 # sprintf("%10.2f", $_->owed)
4673 # } @pr_cust_bill ],
4678 =item _items_pkg [ OPTIONS ]
4680 Return line item hashes for each package item on this invoice. Nearly
4683 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4685 The only OPTIONS accepted is 'section', which may point to a hashref
4686 with a key named 'condensed', which may have a true value. If it
4687 does, this method tries to merge identical items into items with
4688 'quantity' equal to the number of items (not the sum of their
4689 separate quantities, for some reason).
4697 warn "$me _items_pkg searching for all package line items\n"
4700 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4702 warn "$me _items_pkg filtering line items\n"
4704 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4706 if ($options{section} && $options{section}->{condensed}) {
4708 warn "$me _items_pkg condensing section\n"
4712 local $Storable::canonical = 1;
4713 foreach ( @items ) {
4715 delete $item->{ref};
4716 delete $item->{ext_description};
4717 my $key = freeze($item);
4718 $itemshash{$key} ||= 0;
4719 $itemshash{$key} ++; # += $item->{quantity};
4721 @items = sort { $a->{description} cmp $b->{description} }
4722 map { my $i = thaw($_);
4723 $i->{quantity} = $itemshash{$_};
4725 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4731 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4738 return 0 unless $a->itemdesc cmp $b->itemdesc;
4739 return -1 if $b->itemdesc eq 'Tax';
4740 return 1 if $a->itemdesc eq 'Tax';
4741 return -1 if $b->itemdesc eq 'Other surcharges';
4742 return 1 if $a->itemdesc eq 'Other surcharges';
4743 $a->itemdesc cmp $b->itemdesc;
4748 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4749 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4752 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4754 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4755 list of hashrefs describing the line items they generate on the invoice.
4757 OPTIONS may include:
4759 format: the invoice format.
4761 escape_function: the function used to escape strings.
4763 format_function: the function used to format CDRs.
4765 section: a hashref containing 'description'; if this is present,
4766 cust_bill_pkg_display records not belonging to this section are
4769 multisection: a flag indicating that this is a multisection invoice,
4770 which does something complicated.
4772 multilocation: a flag to display the location label for the package.
4774 Returns a list of hashrefs, each of which may contain:
4776 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4777 ext_description, which is an arrayref of detail lines to show below
4782 sub _items_cust_bill_pkg {
4784 my $conf = $self->conf;
4785 my $cust_bill_pkgs = shift;
4788 my $format = $opt{format} || '';
4789 my $escape_function = $opt{escape_function} || sub { shift };
4790 my $format_function = $opt{format_function} || '';
4791 my $unsquelched = $opt{unsquelched} || ''; #unused
4792 my $section = $opt{section}->{description} if $opt{section};
4793 my $summary_page = $opt{summary_page} || ''; #unused
4794 my $multilocation = $opt{multilocation} || '';
4795 my $multisection = $opt{multisection} || '';
4796 my $discount_show_always = 0;
4798 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4801 my ($s, $r, $u) = ( undef, undef, undef );
4802 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4805 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4806 if ( $_ && !$cust_bill_pkg->hidden ) {
4807 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4808 $_->{amount} =~ s/^\-0\.00$/0.00/;
4809 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4811 if $_->{amount} != 0
4812 || $discount_show_always
4813 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4814 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4820 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4821 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4824 foreach my $display ( grep { defined($section)
4825 ? $_->section eq $section
4828 #grep { !$_->summary || !$summary_page } # bunk!
4829 grep { !$_->summary || $multisection }
4830 $cust_bill_pkg->cust_bill_pkg_display
4834 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4835 $display->billpkgdisplaynum. "\n"
4838 my $type = $display->type;
4840 my $desc = $cust_bill_pkg->desc;
4841 $desc = substr($desc, 0, $maxlength). '...'
4842 if $format eq 'latex' && length($desc) > $maxlength;
4844 my %details_opt = ( 'format' => $format,
4845 'escape_function' => $escape_function,
4846 'format_function' => $format_function,
4849 if ( $cust_bill_pkg->pkgnum > 0 ) {
4851 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4854 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4856 # start/end dates for invoice formats that do nonstandard
4858 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4860 if ( (!$type || $type eq 'S')
4861 && ( $cust_bill_pkg->setup != 0
4862 || $cust_bill_pkg->setup_show_zero
4867 warn "$me _items_cust_bill_pkg adding setup\n"
4870 my $description = $desc;
4871 $description .= ' Setup'
4872 if $cust_bill_pkg->recur != 0
4873 || $discount_show_always
4874 || $cust_bill_pkg->recur_show_zero;
4877 unless ( $cust_pkg->part_pkg->hide_svc_detail
4878 || $cust_bill_pkg->hidden )
4881 push @d, map &{$escape_function}($_),
4882 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4883 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4885 if ( $multilocation ) {
4886 my $loc = $cust_pkg->location_label;
4887 $loc = substr($loc, 0, $maxlength). '...'
4888 if $format eq 'latex' && length($loc) > $maxlength;
4889 push @d, &{$escape_function}($loc);
4892 } #unless hiding service details
4894 push @d, $cust_bill_pkg->details(%details_opt)
4895 if $cust_bill_pkg->recur == 0;
4897 if ( $cust_bill_pkg->hidden ) {
4898 $s->{amount} += $cust_bill_pkg->setup;
4899 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4900 push @{ $s->{ext_description} }, @d;
4904 description => $description,
4905 #pkgpart => $part_pkg->pkgpart,
4906 pkgnum => $cust_bill_pkg->pkgnum,
4907 amount => $cust_bill_pkg->setup,
4908 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4909 unit_amount => $cust_bill_pkg->unitsetup,
4910 quantity => $cust_bill_pkg->quantity,
4911 ext_description => \@d,
4917 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4919 $cust_bill_pkg->recur != 0
4920 || $cust_bill_pkg->setup == 0
4921 || $discount_show_always
4922 || $cust_bill_pkg->recur_show_zero
4927 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4930 my $is_summary = $display->summary;
4931 my $description = ($is_summary && $type && $type eq 'U')
4932 ? "Usage charges" : $desc;
4934 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4935 " - ". time2str($date_format, $cust_bill_pkg->edate).
4937 unless $conf->exists('disable_line_item_date_ranges')
4938 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4941 my @seconds = (); # for display of usage info
4943 #at least until cust_bill_pkg has "past" ranges in addition to
4944 #the "future" sdate/edate ones... see #3032
4945 my @dates = ( $self->_date );
4946 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4947 push @dates, $prev->sdate if $prev;
4948 push @dates, undef if !$prev;
4950 unless ( $cust_pkg->part_pkg->hide_svc_detail
4951 || $cust_bill_pkg->itemdesc
4952 || $cust_bill_pkg->hidden
4953 || $is_summary && $type && $type eq 'U' )
4956 warn "$me _items_cust_bill_pkg adding service details\n"
4959 push @d, map &{$escape_function}($_),
4960 $cust_pkg->h_labels_short(@dates, 'I')
4961 #$cust_bill_pkg->edate,
4962 #$cust_bill_pkg->sdate)
4963 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4965 warn "$me _items_cust_bill_pkg done adding service details\n"
4968 if ( $multilocation ) {
4969 my $loc = $cust_pkg->location_label;
4970 $loc = substr($loc, 0, $maxlength). '...'
4971 if $format eq 'latex' && length($loc) > $maxlength;
4972 push @d, &{$escape_function}($loc);
4975 # Display of seconds_since_sqlradacct:
4976 # On the invoice, when processing @detail_items, look for a field
4977 # named 'seconds'. This will contain total seconds for each
4978 # service, in the same order as @ext_description. For services
4979 # that don't support this it will show undef.
4980 if ( $conf->exists('svc_acct-usage_seconds')
4981 and ! $cust_bill_pkg->pkgpart_override ) {
4982 foreach my $cust_svc (
4983 $cust_pkg->h_cust_svc(@dates, 'I')
4986 # eval because not having any part_export_usage exports
4987 # is a fatal error, last_bill/_date because that's how
4988 # sqlradius_hour billing does it
4990 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4992 push @seconds, $sec;
4994 } #if svc_acct-usage_seconds
4998 unless ( $is_summary ) {
4999 warn "$me _items_cust_bill_pkg adding details\n"
5002 #instead of omitting details entirely in this case (unwanted side
5003 # effects), just omit CDRs
5004 $details_opt{'format_function'} = sub { () }
5005 if $type && $type eq 'R';
5007 push @d, $cust_bill_pkg->details(%details_opt);
5010 warn "$me _items_cust_bill_pkg calculating amount\n"
5015 $amount = $cust_bill_pkg->recur;
5016 } elsif ($type eq 'R') {
5017 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5018 } elsif ($type eq 'U') {
5019 $amount = $cust_bill_pkg->usage;
5022 if ( !$type || $type eq 'R' ) {
5024 warn "$me _items_cust_bill_pkg adding recur\n"
5027 if ( $cust_bill_pkg->hidden ) {
5028 $r->{amount} += $amount;
5029 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5030 push @{ $r->{ext_description} }, @d;
5033 description => $description,
5034 #pkgpart => $part_pkg->pkgpart,
5035 pkgnum => $cust_bill_pkg->pkgnum,
5037 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5038 unit_amount => $cust_bill_pkg->unitrecur,
5039 quantity => $cust_bill_pkg->quantity,
5041 ext_description => \@d,
5043 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5046 } else { # $type eq 'U'
5048 warn "$me _items_cust_bill_pkg adding usage\n"
5051 if ( $cust_bill_pkg->hidden ) {
5052 $u->{amount} += $amount;
5053 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5054 push @{ $u->{ext_description} }, @d;
5057 description => $description,
5058 #pkgpart => $part_pkg->pkgpart,
5059 pkgnum => $cust_bill_pkg->pkgnum,
5061 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5062 unit_amount => $cust_bill_pkg->unitrecur,
5063 quantity => $cust_bill_pkg->quantity,
5065 ext_description => \@d,
5070 } # recurring or usage with recurring charge
5072 } else { #pkgnum tax or one-shot line item (??)
5074 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5077 if ( $cust_bill_pkg->setup != 0 ) {
5079 'description' => $desc,
5080 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5083 if ( $cust_bill_pkg->recur != 0 ) {
5085 'description' => "$desc (".
5086 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5087 time2str($date_format, $cust_bill_pkg->edate). ')',
5088 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5096 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5097 && $conf->exists('discount-show-always'));
5101 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5103 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5104 $_->{amount} =~ s/^\-0\.00$/0.00/;
5105 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5107 if $_->{amount} != 0
5108 || $discount_show_always
5109 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5110 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5114 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5121 sub _items_credits {
5122 my( $self, %opt ) = @_;
5123 my $trim_len = $opt{'trim_len'} || 60;
5127 foreach ( $self->cust_credited ) {
5129 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5131 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5132 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5133 $reason = " ($reason) " if $reason;
5136 #'description' => 'Credit ref\#'. $_->crednum.
5137 # " (". time2str("%x",$_->cust_credit->_date) .")".
5139 'description' => $self->mt('Credit applied').' '.
5140 time2str($date_format,$_->cust_credit->_date). $reason,
5141 'amount' => sprintf("%.2f",$_->amount),
5149 sub _items_payments {
5153 #get & print payments
5154 foreach ( $self->cust_bill_pay ) {
5156 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5159 'description' => $self->mt('Payment received').' '.
5160 time2str($date_format,$_->cust_pay->_date ),
5161 'amount' => sprintf("%.2f", $_->amount )
5169 =item _items_discounts_avail
5171 Returns an array of line item hashrefs representing available term discounts
5172 for this invoice. This makes the same assumptions that apply to term
5173 discounts in general: that the package is billed monthly, at a flat rate,
5174 with no usage charges. A prorated first month will be handled, as will
5175 a setup fee if the discount is allowed to apply to setup fees.
5179 sub _items_discounts_avail {
5182 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5184 my ($previous_balance) = $self->previous;
5186 foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5187 $terms{$_->months} = {
5189 base => $previous_balance || 0, # pre-discount sum of charges
5190 discounted => $previous_balance || 0, # post-discount sum
5191 list_pkgnums => 0, # whether any packages are not discounted
5194 foreach my $months (keys %terms) {
5195 my $hash = $terms{$months};
5197 # tricky, because packages may not all be eligible for the same discounts
5198 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5199 my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5200 my $part_pkg = $cust_pkg->part_pkg or next;
5202 next if $part_pkg->freq ne '1';
5203 my $setup = $cust_bill_pkg->setup || 0;
5204 my $recur = $cust_bill_pkg->recur || 0;
5205 my $permonth = $part_pkg->base_recur_permonth || 0;
5207 my ($discount) = grep { $_->months == $months }
5208 map { $_->discount } $part_pkg->part_pkg_discount;
5210 $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5214 if ( $discount->setup ) {
5215 $discountable += $setup;
5218 $hash->{discounted} += $setup;
5221 if ( $discount->percent ) {
5222 $discountable += $months * $permonth;
5223 $discountable -= ($discountable * $discount->percent / 100);
5224 $discountable -= ($permonth - $recur); # correct for prorate
5225 $hash->{discounted} += $discountable;
5228 $discountable += $recur;
5229 $discountable -= $discount->amount * $recur/$permonth;
5231 $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5234 $hash->{discounted} += $discountable;
5235 push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5238 $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5239 $hash->{list_pkgnums} = 1;
5241 } #foreach $cust_bill_pkg
5243 # don't show this line if no packages have discounts at this term
5244 # or if there are no new charges to apply the discount to
5245 delete $terms{$months} if $hash->{base} == $hash->{discounted}
5246 or $hash->{base} == 0;
5250 $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5252 foreach my $months (keys %terms) {
5253 my $hash = $terms{$months};
5254 my $term_total = sprintf('%.2f', $hash->{discounted});
5255 # possibly shouldn't include previous balance in these?
5256 my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5257 my $permonth = sprintf('%.2f', $term_total / $months);
5259 $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5262 $hash->{amount} = $self->mt('[_1] ([_2] per month)',
5263 $term_total, $money_char.$permonth
5267 if ( $list_pkgnums ) {
5268 push @detail, $self->mt('discount on item'). ' '.
5269 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5271 $hash->{ext_description} = join ', ', @detail;
5274 map { $terms{$_} } sort {$b <=> $a} keys %terms;
5277 =item call_details [ OPTION => VALUE ... ]
5279 Returns an array of CSV strings representing the call details for this invoice
5280 The only option available is the boolean prepend_billed_number
5285 my ($self, %opt) = @_;
5287 my $format_function = sub { shift };
5289 if ($opt{prepend_billed_number}) {
5290 $format_function = sub {
5294 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5299 my @details = map { $_->details( 'format_function' => $format_function,
5300 'escape_function' => sub{ return() },
5304 $self->cust_bill_pkg;
5305 my $header = $details[0];
5306 ( $header, grep { $_ ne $header } @details );
5316 =item process_reprint
5320 sub process_reprint {
5321 process_re_X('print', @_);
5324 =item process_reemail
5328 sub process_reemail {
5329 process_re_X('email', @_);
5337 process_re_X('fax', @_);
5345 process_re_X('ftp', @_);
5352 sub process_respool {
5353 process_re_X('spool', @_);
5356 use Storable qw(thaw);
5360 my( $method, $job ) = ( shift, shift );
5361 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5363 my $param = thaw(decode_base64(shift));
5364 warn Dumper($param) if $DEBUG;
5375 my($method, $job, %param ) = @_;
5377 warn "re_X $method for job $job with param:\n".
5378 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5381 #some false laziness w/search/cust_bill.html
5383 my $orderby = 'ORDER BY cust_bill._date';
5385 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5387 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5389 my @cust_bill = qsearch( {
5390 #'select' => "cust_bill.*",
5391 'table' => 'cust_bill',
5392 'addl_from' => $addl_from,
5394 'extra_sql' => $extra_sql,
5395 'order_by' => $orderby,
5399 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5401 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5404 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5405 foreach my $cust_bill ( @cust_bill ) {
5406 $cust_bill->$method();
5408 if ( $job ) { #progressbar foo
5410 if ( time - $min_sec > $last ) {
5411 my $error = $job->update_statustext(
5412 int( 100 * $num / scalar(@cust_bill) )
5414 die $error if $error;
5425 =head1 CLASS METHODS
5431 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5436 my ($class, $start, $end) = @_;
5438 $class->paid_sql($start, $end). ' - '.
5439 $class->credited_sql($start, $end);
5444 Returns an SQL fragment to retreive the net amount (charged minus credited).
5449 my ($class, $start, $end) = @_;
5450 'charged - '. $class->credited_sql($start, $end);
5455 Returns an SQL fragment to retreive the amount paid against this invoice.
5460 my ($class, $start, $end) = @_;
5461 $start &&= "AND cust_bill_pay._date <= $start";
5462 $end &&= "AND cust_bill_pay._date > $end";
5463 $start = '' unless defined($start);
5464 $end = '' unless defined($end);
5465 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5466 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5471 Returns an SQL fragment to retreive the amount credited against this invoice.
5476 my ($class, $start, $end) = @_;
5477 $start &&= "AND cust_credit_bill._date <= $start";
5478 $end &&= "AND cust_credit_bill._date > $end";
5479 $start = '' unless defined($start);
5480 $end = '' unless defined($end);
5481 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5482 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5487 Returns an SQL fragment to retrieve the due date of an invoice.
5488 Currently only supported on PostgreSQL.
5493 my $conf = new FS::Conf;
5497 cust_bill.invoice_terms,
5498 cust_main.invoice_terms,
5499 \''.($conf->config('invoice_default_terms') || '').'\'
5500 ), E\'Net (\\\\d+)\'
5502 ) * 86400 + cust_bill._date'
5505 =item search_sql_where HASHREF
5507 Class method which returns an SQL WHERE fragment to search for parameters
5508 specified in HASHREF. Valid parameters are
5514 List reference of start date, end date, as UNIX timestamps.
5524 List reference of charged limits (exclusive).
5528 List reference of charged limits (exclusive).
5532 flag, return open invoices only
5536 flag, return net invoices only
5540 =item newest_percust
5544 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5548 sub search_sql_where {
5549 my($class, $param) = @_;
5551 warn "$me search_sql_where called with params: \n".
5552 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5558 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5559 push @search, "cust_main.agentnum = $1";
5563 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5564 push @search, "cust_bill.custnum = $1";
5568 if ( $param->{_date} ) {
5569 my($beginning, $ending) = @{$param->{_date}};
5571 push @search, "cust_bill._date >= $beginning",
5572 "cust_bill._date < $ending";
5576 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5577 push @search, "cust_bill.invnum >= $1";
5579 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5580 push @search, "cust_bill.invnum <= $1";
5584 if ( $param->{charged} ) {
5585 my @charged = ref($param->{charged})
5586 ? @{ $param->{charged} }
5587 : ($param->{charged});
5589 push @search, map { s/^charged/cust_bill.charged/; $_; }
5593 my $owed_sql = FS::cust_bill->owed_sql;
5596 if ( $param->{owed} ) {
5597 my @owed = ref($param->{owed})
5598 ? @{ $param->{owed} }
5600 push @search, map { s/^owed/$owed_sql/; $_; }
5605 push @search, "0 != $owed_sql"
5606 if $param->{'open'};
5607 push @search, '0 != '. FS::cust_bill->net_sql
5611 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5612 if $param->{'days'};
5615 if ( $param->{'newest_percust'} ) {
5617 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5618 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5620 my @newest_where = map { my $x = $_;
5621 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5624 grep ! /^cust_main./, @search;
5625 my $newest_where = scalar(@newest_where)
5626 ? ' AND '. join(' AND ', @newest_where)
5630 push @search, "cust_bill._date = (
5631 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5632 WHERE newest_cust_bill.custnum = cust_bill.custnum
5638 #agent virtualization
5639 my $curuser = $FS::CurrentUser::CurrentUser;
5640 if ( $curuser->username eq 'fs_queue'
5641 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5643 my $newuser = qsearchs('access_user', {
5644 'username' => $username,
5648 $curuser = $newuser;
5650 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5653 push @search, $curuser->agentnums_sql;
5655 join(' AND ', @search );
5667 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5668 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base