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(
254 foreach my $linked ( $self->$table() ) {
255 my $error = $linked->delete;
257 $dbh->rollback if $oldAutoCommit;
264 my $error = $self->SUPER::delete(@_);
266 $dbh->rollback if $oldAutoCommit;
270 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
276 =item replace [ OLD_RECORD ]
278 You can, but probably shouldn't modify invoices...
280 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
281 supplied, replaces this record. If there is an error, returns the error,
282 otherwise returns false.
286 #replace can be inherited from Record.pm
288 # replace_check is now the preferred way to #implement replace data checks
289 # (so $object->replace() works without an argument)
292 my( $new, $old ) = ( shift, shift );
293 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
294 #return "Can't change _date!" unless $old->_date eq $new->_date;
295 return "Can't change _date" unless $old->_date == $new->_date;
296 return "Can't change charged" unless $old->charged == $new->charged
297 || $old->charged == 0
298 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
304 =item add_cc_surcharge
310 sub add_cc_surcharge {
311 my ($self, $pkgnum, $amount) = (shift, shift, shift);
314 my $cust_bill_pkg = new FS::cust_bill_pkg({
315 'invnum' => $self->invnum,
319 $error = $cust_bill_pkg->insert;
320 return $error if $error;
322 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
323 $self->charged($self->charged+$amount);
324 $error = $self->replace;
325 return $error if $error;
327 $self->apply_payments_and_credits;
333 Checks all fields to make sure this is a valid invoice. If there is an error,
334 returns the error, otherwise returns false. Called by the insert and replace
343 $self->ut_numbern('invnum')
344 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
345 || $self->ut_numbern('_date')
346 || $self->ut_money('charged')
347 || $self->ut_numbern('printed')
348 || $self->ut_enum('closed', [ '', 'Y' ])
349 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
350 || $self->ut_numbern('agent_invid') #varchar?
352 return $error if $error;
354 $self->_date(time) unless $self->_date;
356 $self->printed(0) if $self->printed eq '';
363 Returns the displayed invoice number for this invoice: agent_invid if
364 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
370 my $conf = $self->conf;
371 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
372 return $self->agent_invid;
374 return $self->invnum;
380 Returns a list consisting of the total previous balance for this customer,
381 followed by the previous outstanding invoices (as FS::cust_bill objects also).
388 my @cust_bill = sort { $a->_date <=> $b->_date }
389 grep { $_->owed != 0 && $_->_date < $self->_date }
390 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
392 foreach ( @cust_bill ) { $total += $_->owed; }
398 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
405 { 'table' => 'cust_bill_pkg',
406 'hashref' => { 'invnum' => $self->invnum },
407 'order_by' => 'ORDER BY billpkgnum',
412 =item cust_bill_pkg_pkgnum PKGNUM
414 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
419 sub cust_bill_pkg_pkgnum {
420 my( $self, $pkgnum ) = @_;
422 { 'table' => 'cust_bill_pkg',
423 'hashref' => { 'invnum' => $self->invnum,
426 'order_by' => 'ORDER BY billpkgnum',
433 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
440 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
441 $self->cust_bill_pkg;
443 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
448 Returns true if any of the packages (or their definitions) corresponding to the
449 line items for this invoice have the no_auto flag set.
455 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
458 =item open_cust_bill_pkg
460 Returns the open line items for this invoice.
462 Note that cust_bill_pkg with both setup and recur fees are returned as two
463 separate line items, each with only one fee.
467 # modeled after cust_main::open_cust_bill
468 sub open_cust_bill_pkg {
471 # grep { $_->owed > 0 } $self->cust_bill_pkg
473 my %other = ( 'recur' => 'setup',
474 'setup' => 'recur', );
476 foreach my $field ( qw( recur setup )) {
477 push @open, map { $_->set( $other{$field}, 0 ); $_; }
478 grep { $_->owed($field) > 0 }
479 $self->cust_bill_pkg;
485 =item cust_bill_event
487 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
491 sub cust_bill_event {
493 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
496 =item num_cust_bill_event
498 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
502 sub num_cust_bill_event {
505 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
506 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
507 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
508 $sth->fetchrow_arrayref->[0];
513 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
517 #false laziness w/cust_pkg.pm
521 'table' => 'cust_event',
522 'addl_from' => 'JOIN part_event USING ( eventpart )',
523 'hashref' => { 'tablenum' => $self->invnum },
524 'extra_sql' => " AND eventtable = 'cust_bill' ",
530 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
534 #false laziness w/cust_pkg.pm
538 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
539 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
540 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
541 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
542 $sth->fetchrow_arrayref->[0];
547 Returns the customer (see L<FS::cust_main>) for this invoice.
553 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
556 =item cust_suspend_if_balance_over AMOUNT
558 Suspends the customer associated with this invoice if the total amount owed on
559 this invoice and all older invoices is greater than the specified amount.
561 Returns a list: an empty list on success or a list of errors.
565 sub cust_suspend_if_balance_over {
566 my( $self, $amount ) = ( shift, shift );
567 my $cust_main = $self->cust_main;
568 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
571 $cust_main->suspend(@_);
577 Depreciated. See the cust_credited method.
579 #Returns a list consisting of the total previous credited (see
580 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
581 #outstanding credits (FS::cust_credit objects).
587 croak "FS::cust_bill->cust_credit depreciated; see ".
588 "FS::cust_bill->cust_credit_bill";
591 #my @cust_credit = sort { $a->_date <=> $b->_date }
592 # grep { $_->credited != 0 && $_->_date < $self->_date }
593 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
595 #foreach (@cust_credit) { $total += $_->credited; }
596 #$total, @cust_credit;
601 Depreciated. See the cust_bill_pay method.
603 #Returns all payments (see L<FS::cust_pay>) for this invoice.
609 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
611 #sort { $a->_date <=> $b->_date }
612 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
618 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
621 sub cust_bill_pay_batch {
623 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
628 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
634 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
635 sort { $a->_date <=> $b->_date }
636 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
641 =item cust_credit_bill
643 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
649 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
650 sort { $a->_date <=> $b->_date }
651 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
655 sub cust_credit_bill {
656 shift->cust_credited(@_);
659 #=item cust_bill_pay_pkgnum PKGNUM
661 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
662 #with matching pkgnum.
666 #sub cust_bill_pay_pkgnum {
667 # my( $self, $pkgnum ) = @_;
668 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
669 # sort { $a->_date <=> $b->_date }
670 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
671 # 'pkgnum' => $pkgnum,
676 =item cust_bill_pay_pkg PKGNUM
678 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
679 applied against the matching pkgnum.
683 sub cust_bill_pay_pkg {
684 my( $self, $pkgnum ) = @_;
687 'select' => 'cust_bill_pay_pkg.*',
688 'table' => 'cust_bill_pay_pkg',
689 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
690 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
691 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
692 " AND cust_bill_pkg.pkgnum = $pkgnum",
697 #=item cust_credited_pkgnum PKGNUM
699 #=item cust_credit_bill_pkgnum PKGNUM
701 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
702 #with matching pkgnum.
706 #sub cust_credited_pkgnum {
707 # my( $self, $pkgnum ) = @_;
708 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
709 # sort { $a->_date <=> $b->_date }
710 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
711 # 'pkgnum' => $pkgnum,
716 #sub cust_credit_bill_pkgnum {
717 # shift->cust_credited_pkgnum(@_);
720 =item cust_credit_bill_pkg PKGNUM
722 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
723 applied against the matching pkgnum.
727 sub cust_credit_bill_pkg {
728 my( $self, $pkgnum ) = @_;
731 'select' => 'cust_credit_bill_pkg.*',
732 'table' => 'cust_credit_bill_pkg',
733 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
734 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
735 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
736 " AND cust_bill_pkg.pkgnum = $pkgnum",
741 =item cust_bill_batch
743 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
747 sub cust_bill_batch {
749 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
754 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
761 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
763 foreach (@taxlines) { $total += $_->setup; }
769 Returns the amount owed (still outstanding) on this invoice, which is charged
770 minus all payment applications (see L<FS::cust_bill_pay>) and credit
771 applications (see L<FS::cust_credit_bill>).
777 my $balance = $self->charged;
778 $balance -= $_->amount foreach ( $self->cust_bill_pay );
779 $balance -= $_->amount foreach ( $self->cust_credited );
780 $balance = sprintf( "%.2f", $balance);
781 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
786 my( $self, $pkgnum ) = @_;
788 #my $balance = $self->charged;
790 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
792 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
793 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
795 $balance = sprintf( "%.2f", $balance);
796 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
800 =item apply_payments_and_credits [ OPTION => VALUE ... ]
802 Applies unapplied payments and credits to this invoice.
804 A hash of optional arguments may be passed. Currently "manual" is supported.
805 If true, a payment receipt is sent instead of a statement when
806 'payment_receipt_email' configuration option is set.
808 If there is an error, returns the error, otherwise returns false.
812 sub apply_payments_and_credits {
813 my( $self, %options ) = @_;
814 my $conf = $self->conf;
816 local $SIG{HUP} = 'IGNORE';
817 local $SIG{INT} = 'IGNORE';
818 local $SIG{QUIT} = 'IGNORE';
819 local $SIG{TERM} = 'IGNORE';
820 local $SIG{TSTP} = 'IGNORE';
821 local $SIG{PIPE} = 'IGNORE';
823 my $oldAutoCommit = $FS::UID::AutoCommit;
824 local $FS::UID::AutoCommit = 0;
827 $self->select_for_update; #mutex
829 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
830 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
832 if ( $conf->exists('pkg-balances') ) {
833 # limit @payments & @credits to those w/ a pkgnum grepped from $self
834 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
835 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
836 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
839 while ( $self->owed > 0 and ( @payments || @credits ) ) {
842 if ( @payments && @credits ) {
844 #decide which goes first by weight of top (unapplied) line item
846 my @open_lineitems = $self->open_cust_bill_pkg;
849 max( map { $_->part_pkg->pay_weight || 0 }
854 my $max_credit_weight =
855 max( map { $_->part_pkg->credit_weight || 0 }
861 #if both are the same... payments first? it has to be something
862 if ( $max_pay_weight >= $max_credit_weight ) {
868 } elsif ( @payments ) {
870 } elsif ( @credits ) {
873 die "guru meditation #12 and 35";
877 if ( $app eq 'pay' ) {
879 my $payment = shift @payments;
880 $unapp_amount = $payment->unapplied;
881 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
882 $app->pkgnum( $payment->pkgnum )
883 if $conf->exists('pkg-balances') && $payment->pkgnum;
885 } elsif ( $app eq 'credit' ) {
887 my $credit = shift @credits;
888 $unapp_amount = $credit->credited;
889 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
890 $app->pkgnum( $credit->pkgnum )
891 if $conf->exists('pkg-balances') && $credit->pkgnum;
894 die "guru meditation #12 and 35";
898 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
899 warn "owed_pkgnum ". $app->pkgnum;
900 $owed = $self->owed_pkgnum($app->pkgnum);
904 next unless $owed > 0;
906 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
907 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
909 $app->invnum( $self->invnum );
911 my $error = $app->insert(%options);
913 $dbh->rollback if $oldAutoCommit;
914 return "Error inserting ". $app->table. " record: $error";
916 die $error if $error;
920 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
925 =item generate_email OPTION => VALUE ...
933 sender address, required
937 alternate template name, optional
941 text attachment arrayref, optional
945 email subject, optional
949 notice name instead of "Invoice", optional
953 Returns an argument list to be passed to L<FS::Misc::send_email>.
963 my $conf = $self->conf;
965 my $me = '[FS::cust_bill::generate_email]';
968 'from' => $args{'from'},
969 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
973 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
974 'template' => $args{'template'},
975 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
976 'no_coupon' => $args{'no_coupon'},
979 my $cust_main = $self->cust_main;
981 if (ref($args{'to'}) eq 'ARRAY') {
982 $return{'to'} = $args{'to'};
984 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
985 $cust_main->invoicing_list
989 if ( $conf->exists('invoice_html') ) {
991 warn "$me creating HTML/text multipart message"
994 $return{'nobody'} = 1;
996 my $alternative = build MIME::Entity
997 'Type' => 'multipart/alternative',
998 #'Encoding' => '7bit',
999 'Disposition' => 'inline'
1003 if ( $conf->exists('invoice_email_pdf')
1004 and scalar($conf->config('invoice_email_pdf_note')) ) {
1006 warn "$me using 'invoice_email_pdf_note' in multipart message"
1008 $data = [ map { $_ . "\n" }
1009 $conf->config('invoice_email_pdf_note')
1014 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1016 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1017 $data = $args{'print_text'};
1019 $data = [ $self->print_text(\%opt) ];
1024 $alternative->attach(
1025 'Type' => 'text/plain',
1026 'Encoding' => 'quoted-printable',
1027 #'Encoding' => '7bit',
1029 'Disposition' => 'inline',
1032 $args{'from'} =~ /\@([\w\.\-]+)/;
1033 my $from = $1 || 'example.com';
1034 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1037 my $agentnum = $cust_main->agentnum;
1038 if ( defined($args{'template'}) && length($args{'template'})
1039 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1042 $logo = 'logo_'. $args{'template'}. '.png';
1046 my $image_data = $conf->config_binary( $logo, $agentnum);
1048 my $image = build MIME::Entity
1049 'Type' => 'image/png',
1050 'Encoding' => 'base64',
1051 'Data' => $image_data,
1052 'Filename' => 'logo.png',
1053 'Content-ID' => "<$content_id>",
1057 if($conf->exists('invoice-barcode')){
1058 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1059 $barcode = build MIME::Entity
1060 'Type' => 'image/png',
1061 'Encoding' => 'base64',
1062 'Data' => $self->invoice_barcode(0),
1063 'Filename' => 'barcode.png',
1064 'Content-ID' => "<$barcode_content_id>",
1066 $opt{'barcode_cid'} = $barcode_content_id;
1069 $alternative->attach(
1070 'Type' => 'text/html',
1071 'Encoding' => 'quoted-printable',
1072 'Data' => [ '<html>',
1075 ' '. encode_entities($return{'subject'}),
1078 ' <body bgcolor="#e8e8e8">',
1079 $self->print_html({ 'cid'=>$content_id, %opt }),
1083 'Disposition' => 'inline',
1084 #'Filename' => 'invoice.pdf',
1087 my @otherparts = ();
1088 if ( $cust_main->email_csv_cdr ) {
1090 push @otherparts, build MIME::Entity
1091 'Type' => 'text/csv',
1092 'Encoding' => '7bit',
1093 'Data' => [ map { "$_\n" }
1094 $self->call_details('prepend_billed_number' => 1)
1096 'Disposition' => 'attachment',
1097 'Filename' => 'usage-'. $self->invnum. '.csv',
1102 if ( $conf->exists('invoice_email_pdf') ) {
1107 # multipart/alternative
1113 my $related = build MIME::Entity 'Type' => 'multipart/related',
1114 'Encoding' => '7bit';
1116 #false laziness w/Misc::send_email
1117 $related->head->replace('Content-type',
1118 $related->mime_type.
1119 '; boundary="'. $related->head->multipart_boundary. '"'.
1120 '; type=multipart/alternative'
1123 $related->add_part($alternative);
1125 $related->add_part($image);
1127 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1129 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1133 #no other attachment:
1135 # multipart/alternative
1140 $return{'content-type'} = 'multipart/related';
1141 if($conf->exists('invoice-barcode')){
1142 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1145 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1147 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1148 #$return{'disposition'} = 'inline';
1154 if ( $conf->exists('invoice_email_pdf') ) {
1155 warn "$me creating PDF attachment"
1158 #mime parts arguments a la MIME::Entity->build().
1159 $return{'mimeparts'} = [
1160 { $self->mimebuild_pdf(\%opt) }
1164 if ( $conf->exists('invoice_email_pdf')
1165 and scalar($conf->config('invoice_email_pdf_note')) ) {
1167 warn "$me using 'invoice_email_pdf_note'"
1169 $return{'body'} = [ map { $_ . "\n" }
1170 $conf->config('invoice_email_pdf_note')
1175 warn "$me not using 'invoice_email_pdf_note'"
1177 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1178 $return{'body'} = $args{'print_text'};
1180 $return{'body'} = [ $self->print_text(\%opt) ];
1193 Returns a list suitable for passing to MIME::Entity->build(), representing
1194 this invoice as PDF attachment.
1201 'Type' => 'application/pdf',
1202 'Encoding' => 'base64',
1203 'Data' => [ $self->print_pdf(@_) ],
1204 'Disposition' => 'attachment',
1205 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1209 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1211 Sends this invoice to the destinations configured for this customer: sends
1212 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1214 Options can be passed as a hashref (recommended) or as a list of up to
1215 four values for templatename, agentnum, invoice_from and amount.
1217 I<template>, if specified, is the name of a suffix for alternate invoices.
1219 I<agentnum>, if specified, means that this invoice will only be sent for customers
1220 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1221 single agent) or an arrayref of agentnums.
1223 I<invoice_from>, if specified, overrides the default email invoice From: address.
1225 I<amount>, if specified, only sends the invoice if the total amount owed on this
1226 invoice and all older invoices is greater than the specified amount.
1228 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1232 sub queueable_send {
1235 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1236 or die "invalid invoice number: " . $opt{invnum};
1238 my @args = ( $opt{template}, $opt{agentnum} );
1239 push @args, $opt{invoice_from}
1240 if exists($opt{invoice_from}) && $opt{invoice_from};
1242 my $error = $self->send( @args );
1243 die $error if $error;
1249 my $conf = $self->conf;
1251 my( $template, $invoice_from, $notice_name );
1253 my $balance_over = 0;
1257 $template = $opt->{'template'} || '';
1258 if ( $agentnums = $opt->{'agentnum'} ) {
1259 $agentnums = [ $agentnums ] unless ref($agentnums);
1261 $invoice_from = $opt->{'invoice_from'};
1262 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1263 $notice_name = $opt->{'notice_name'};
1265 $template = scalar(@_) ? shift : '';
1266 if ( scalar(@_) && $_[0] ) {
1267 $agentnums = ref($_[0]) ? shift : [ shift ];
1269 $invoice_from = shift if scalar(@_);
1270 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1273 return 'N/A' unless ! $agentnums
1274 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1277 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1279 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1280 $conf->config('invoice_from', $self->cust_main->agentnum );
1283 'template' => $template,
1284 'invoice_from' => $invoice_from,
1285 'notice_name' => ( $notice_name || 'Invoice' ),
1288 my @invoicing_list = $self->cust_main->invoicing_list;
1290 #$self->email_invoice(\%opt)
1292 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1294 #$self->print_invoice(\%opt)
1296 if grep { $_ eq 'POST' } @invoicing_list; #postal
1298 $self->fax_invoice(\%opt)
1299 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1305 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1307 Emails this invoice.
1309 Options can be passed as a hashref (recommended) or as a list of up to
1310 two values for templatename and invoice_from.
1312 I<template>, if specified, is the name of a suffix for alternate invoices.
1314 I<invoice_from>, if specified, overrides the default email invoice From: address.
1316 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1320 sub queueable_email {
1323 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1324 or die "invalid invoice number: " . $opt{invnum};
1326 my %args = ( 'template' => $opt{template} );
1327 $args{$_} = $opt{$_}
1328 foreach grep { exists($opt{$_}) && $opt{$_} }
1329 qw( invoice_from notice_name no_coupon );
1331 my $error = $self->email( \%args );
1332 die $error if $error;
1336 #sub email_invoice {
1339 my $conf = $self->conf;
1341 my( $template, $invoice_from, $notice_name, $no_coupon );
1344 $template = $opt->{'template'} || '';
1345 $invoice_from = $opt->{'invoice_from'};
1346 $notice_name = $opt->{'notice_name'} || 'Invoice';
1347 $no_coupon = $opt->{'no_coupon'} || 0;
1349 $template = scalar(@_) ? shift : '';
1350 $invoice_from = shift if scalar(@_);
1351 $notice_name = 'Invoice';
1355 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1356 $conf->config('invoice_from', $self->cust_main->agentnum );
1358 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1359 $self->cust_main->invoicing_list;
1361 if ( ! @invoicing_list ) { #no recipients
1362 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1363 die 'No recipients for customer #'. $self->custnum;
1365 #default: better to notify this person than silence
1366 @invoicing_list = ($invoice_from);
1370 my $subject = $self->email_subject($template);
1372 my $error = send_email(
1373 $self->generate_email(
1374 'from' => $invoice_from,
1375 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1376 'subject' => $subject,
1377 'template' => $template,
1378 'notice_name' => $notice_name,
1379 'no_coupon' => $no_coupon,
1382 die "can't email invoice: $error\n" if $error;
1383 #die "$error\n" if $error;
1389 my $conf = $self->conf;
1391 #my $template = scalar(@_) ? shift : '';
1394 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1397 my $cust_main = $self->cust_main;
1398 my $name = $cust_main->name;
1399 my $name_short = $cust_main->name_short;
1400 my $invoice_number = $self->invnum;
1401 my $invoice_date = $self->_date_pretty;
1403 eval qq("$subject");
1406 =item lpr_data HASHREF | [ TEMPLATE ]
1408 Returns the postscript or plaintext for this invoice as an arrayref.
1410 Options can be passed as a hashref (recommended) or as a single optional value
1413 I<template>, if specified, is the name of a suffix for alternate invoices.
1415 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1421 my $conf = $self->conf;
1422 my( $template, $notice_name );
1425 $template = $opt->{'template'} || '';
1426 $notice_name = $opt->{'notice_name'} || 'Invoice';
1428 $template = scalar(@_) ? shift : '';
1429 $notice_name = 'Invoice';
1433 'template' => $template,
1434 'notice_name' => $notice_name,
1437 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1438 [ $self->$method( \%opt ) ];
1441 =item print HASHREF | [ TEMPLATE ]
1443 Prints this invoice.
1445 Options can be passed as a hashref (recommended) or as a single optional
1448 I<template>, if specified, is the name of a suffix for alternate invoices.
1450 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1454 #sub print_invoice {
1457 my $conf = $self->conf;
1458 my( $template, $notice_name );
1461 $template = $opt->{'template'} || '';
1462 $notice_name = $opt->{'notice_name'} || 'Invoice';
1464 $template = scalar(@_) ? shift : '';
1465 $notice_name = 'Invoice';
1469 'template' => $template,
1470 'notice_name' => $notice_name,
1473 if($conf->exists('invoice_print_pdf')) {
1474 # Add the invoice to the current batch.
1475 $self->batch_invoice(\%opt);
1478 do_print $self->lpr_data(\%opt);
1482 =item fax_invoice HASHREF | [ TEMPLATE ]
1486 Options can be passed as a hashref (recommended) or as a single optional
1489 I<template>, if specified, is the name of a suffix for alternate invoices.
1491 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1497 my $conf = $self->conf;
1498 my( $template, $notice_name );
1501 $template = $opt->{'template'} || '';
1502 $notice_name = $opt->{'notice_name'} || 'Invoice';
1504 $template = scalar(@_) ? shift : '';
1505 $notice_name = 'Invoice';
1508 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1509 unless $conf->exists('invoice_latex');
1511 my $dialstring = $self->cust_main->getfield('fax');
1515 'template' => $template,
1516 'notice_name' => $notice_name,
1519 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1520 'dialstring' => $dialstring,
1522 die $error if $error;
1526 =item batch_invoice [ HASHREF ]
1528 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1529 isn't an open batch, one will be created.
1534 my ($self, $opt) = @_;
1535 my $bill_batch = $self->get_open_bill_batch;
1536 my $cust_bill_batch = FS::cust_bill_batch->new({
1537 batchnum => $bill_batch->batchnum,
1538 invnum => $self->invnum,
1540 return $cust_bill_batch->insert($opt);
1543 =item get_open_batch
1545 Returns the currently open batch as an FS::bill_batch object, creating a new
1546 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1551 sub get_open_bill_batch {
1553 my $conf = $self->conf;
1554 my $hashref = { status => 'O' };
1555 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1556 ? $self->cust_main->agentnum
1558 my $batch = qsearchs('bill_batch', $hashref);
1559 return $batch if $batch;
1560 $batch = FS::bill_batch->new($hashref);
1561 my $error = $batch->insert;
1562 die $error if $error;
1566 =item ftp_invoice [ TEMPLATENAME ]
1568 Sends this invoice data via FTP.
1570 TEMPLATENAME is unused?
1576 my $conf = $self->conf;
1577 my $template = scalar(@_) ? shift : '';
1580 'protocol' => 'ftp',
1581 'server' => $conf->config('cust_bill-ftpserver'),
1582 'username' => $conf->config('cust_bill-ftpusername'),
1583 'password' => $conf->config('cust_bill-ftppassword'),
1584 'dir' => $conf->config('cust_bill-ftpdir'),
1585 'format' => $conf->config('cust_bill-ftpformat'),
1589 =item spool_invoice [ TEMPLATENAME ]
1591 Spools this invoice data (see L<FS::spool_csv>)
1593 TEMPLATENAME is unused?
1599 my $conf = $self->conf;
1600 my $template = scalar(@_) ? shift : '';
1603 'format' => $conf->config('cust_bill-spoolformat'),
1604 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1608 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1610 Like B<send>, but only sends the invoice if it is the newest open invoice for
1615 sub send_if_newest {
1620 grep { $_->owed > 0 }
1621 qsearch('cust_bill', {
1622 'custnum' => $self->custnum,
1623 #'_date' => { op=>'>', value=>$self->_date },
1624 'invnum' => { op=>'>', value=>$self->invnum },
1631 =item send_csv OPTION => VALUE, ...
1633 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1637 protocol - currently only "ftp"
1643 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1644 and YYMMDDHHMMSS is a timestamp.
1646 See L</print_csv> for a description of the output format.
1651 my($self, %opt) = @_;
1655 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1656 mkdir $spooldir, 0700 unless -d $spooldir;
1658 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1659 my $file = "$spooldir/$tracctnum.csv";
1661 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1663 open(CSV, ">$file") or die "can't open $file: $!";
1671 if ( $opt{protocol} eq 'ftp' ) {
1672 eval "use Net::FTP;";
1674 $net = Net::FTP->new($opt{server}) or die @$;
1676 die "unknown protocol: $opt{protocol}";
1679 $net->login( $opt{username}, $opt{password} )
1680 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1682 $net->binary or die "can't set binary mode";
1684 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1686 $net->put($file) or die "can't put $file: $!";
1696 Spools CSV invoice data.
1702 =item format - 'default' or 'billco'
1704 =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>).
1706 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1708 =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.
1715 my($self, %opt) = @_;
1717 my $cust_main = $self->cust_main;
1719 if ( $opt{'dest'} ) {
1720 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1721 $cust_main->invoicing_list;
1722 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1723 || ! keys %invoicing_list;
1726 if ( $opt{'balanceover'} ) {
1728 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1731 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1732 mkdir $spooldir, 0700 unless -d $spooldir;
1734 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1738 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1739 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1742 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1744 open(CSV, ">>$file") or die "can't open $file: $!";
1745 flock(CSV, LOCK_EX);
1750 if ( lc($opt{'format'}) eq 'billco' ) {
1752 flock(CSV, LOCK_UN);
1757 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1760 open(CSV,">>$file") or die "can't open $file: $!";
1761 flock(CSV, LOCK_EX);
1767 flock(CSV, LOCK_UN);
1774 =item print_csv OPTION => VALUE, ...
1776 Returns CSV data for this invoice.
1780 format - 'default' or 'billco'
1782 Returns a list consisting of two scalars. The first is a single line of CSV
1783 header information for this invoice. The second is one or more lines of CSV
1784 detail information for this invoice.
1786 If I<format> is not specified or "default", the fields of the CSV file are as
1789 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1793 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1795 B<record_type> is C<cust_bill> for the initial header line only. The
1796 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1797 fields are filled in.
1799 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1800 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1803 =item invnum - invoice number
1805 =item custnum - customer number
1807 =item _date - invoice date
1809 =item charged - total invoice amount
1811 =item first - customer first name
1813 =item last - customer first name
1815 =item company - company name
1817 =item address1 - address line 1
1819 =item address2 - address line 1
1829 =item pkg - line item description
1831 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1833 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1835 =item sdate - start date for recurring fee
1837 =item edate - end date for recurring fee
1841 If I<format> is "billco", the fields of the header CSV file are as follows:
1843 +-------------------------------------------------------------------+
1844 | FORMAT HEADER FILE |
1845 |-------------------------------------------------------------------|
1846 | Field | Description | Name | Type | Width |
1847 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1848 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1849 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1850 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1851 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1852 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1853 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1854 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1855 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1856 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1857 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1858 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1859 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1860 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1861 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1862 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1863 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1864 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1865 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1866 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1867 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1868 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1869 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1870 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1871 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1872 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1873 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1874 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1875 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1876 +-------+-------------------------------+------------+------+-------+
1878 If I<format> is "billco", the fields of the detail CSV file are as follows:
1880 FORMAT FOR DETAIL FILE
1882 Field | Description | Name | Type | Width
1883 1 | N/A-Leave Empty | RC | CHAR | 2
1884 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1885 3 | Account Number | TRACCTNUM | CHAR | 15
1886 4 | Invoice Number | TRINVOICE | CHAR | 15
1887 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1888 6 | Transaction Detail | DETAILS | CHAR | 100
1889 7 | Amount | AMT | NUM* | 9
1890 8 | Line Format Control** | LNCTRL | CHAR | 2
1891 9 | Grouping Code | GROUP | CHAR | 2
1892 10 | User Defined | ACCT CODE | CHAR | 15
1897 my($self, %opt) = @_;
1899 eval "use Text::CSV_XS";
1902 my $cust_main = $self->cust_main;
1904 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1906 if ( lc($opt{'format'}) eq 'billco' ) {
1909 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1911 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1913 my( $previous_balance, @unused ) = $self->previous; #previous balance
1915 my $pmt_cr_applied = 0;
1916 $pmt_cr_applied += $_->{'amount'}
1917 foreach ( $self->_items_payments, $self->_items_credits ) ;
1919 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1922 '', # 1 | N/A-Leave Empty CHAR 2
1923 '', # 2 | N/A-Leave Empty CHAR 15
1924 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1925 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1926 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1927 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1928 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1929 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1930 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1931 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1932 '', # 10 | Ancillary Billing Information CHAR 30
1933 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1934 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1937 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1940 $duedate, # 14 | Bill Due Date CHAR 10
1942 $previous_balance, # 15 | Previous Balance NUM* 9
1943 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1944 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1945 $totaldue, # 18 | Total Amt Due NUM* 9
1946 $totaldue, # 19 | Total Amt Due NUM* 9
1947 '', # 20 | 30 Day Aging NUM* 9
1948 '', # 21 | 60 Day Aging NUM* 9
1949 '', # 22 | 90 Day Aging NUM* 9
1950 'N', # 23 | Y/N CHAR 1
1951 '', # 24 | Remittance automation CHAR 100
1952 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1953 $self->custnum, # 26 | Customer Reference Number CHAR 15
1954 '0', # 27 | Federal Tax*** NUM* 9
1955 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1956 '0', # 29 | Other Taxes & Fees*** NUM* 9
1965 time2str("%x", $self->_date),
1966 sprintf("%.2f", $self->charged),
1967 ( map { $cust_main->getfield($_) }
1968 qw( first last company address1 address2 city state zip country ) ),
1970 ) or die "can't create csv";
1973 my $header = $csv->string. "\n";
1976 if ( lc($opt{'format'}) eq 'billco' ) {
1979 foreach my $item ( $self->_items_pkg ) {
1982 '', # 1 | N/A-Leave Empty CHAR 2
1983 '', # 2 | N/A-Leave Empty CHAR 15
1984 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1985 $self->invnum, # 4 | Invoice Number CHAR 15
1986 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1987 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1988 $item->{'amount'}, # 7 | Amount NUM* 9
1989 '', # 8 | Line Format Control** CHAR 2
1990 '', # 9 | Grouping Code CHAR 2
1991 '', # 10 | User Defined CHAR 15
1994 $detail .= $csv->string. "\n";
2000 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2002 my($pkg, $setup, $recur, $sdate, $edate);
2003 if ( $cust_bill_pkg->pkgnum ) {
2005 ($pkg, $setup, $recur, $sdate, $edate) = (
2006 $cust_bill_pkg->part_pkg->pkg,
2007 ( $cust_bill_pkg->setup != 0
2008 ? sprintf("%.2f", $cust_bill_pkg->setup )
2010 ( $cust_bill_pkg->recur != 0
2011 ? sprintf("%.2f", $cust_bill_pkg->recur )
2013 ( $cust_bill_pkg->sdate
2014 ? time2str("%x", $cust_bill_pkg->sdate)
2016 ($cust_bill_pkg->edate
2017 ?time2str("%x", $cust_bill_pkg->edate)
2021 } else { #pkgnum tax
2022 next unless $cust_bill_pkg->setup != 0;
2023 $pkg = $cust_bill_pkg->desc;
2024 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2025 ( $sdate, $edate ) = ( '', '' );
2031 ( map { '' } (1..11) ),
2032 ($pkg, $setup, $recur, $sdate, $edate)
2033 ) or die "can't create csv";
2035 $detail .= $csv->string. "\n";
2041 ( $header, $detail );
2047 Pays this invoice with a compliemntary payment. If there is an error,
2048 returns the error, otherwise returns false.
2054 my $cust_pay = new FS::cust_pay ( {
2055 'invnum' => $self->invnum,
2056 'paid' => $self->owed,
2059 'payinfo' => $self->cust_main->payinfo,
2067 Attempts to pay this invoice with a credit card payment via a
2068 Business::OnlinePayment realtime gateway. See
2069 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2070 for supported processors.
2076 $self->realtime_bop( 'CC', @_ );
2081 Attempts to pay this invoice with an electronic check (ACH) payment via a
2082 Business::OnlinePayment realtime gateway. See
2083 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2084 for supported processors.
2090 $self->realtime_bop( 'ECHECK', @_ );
2095 Attempts to pay this invoice with phone bill (LEC) payment via a
2096 Business::OnlinePayment realtime gateway. See
2097 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2098 for supported processors.
2104 $self->realtime_bop( 'LEC', @_ );
2108 my( $self, $method ) = (shift,shift);
2109 my $conf = $self->conf;
2112 my $cust_main = $self->cust_main;
2113 my $balance = $cust_main->balance;
2114 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2115 $amount = sprintf("%.2f", $amount);
2116 return "not run (balance $balance)" unless $amount > 0;
2118 my $description = 'Internet Services';
2119 if ( $conf->exists('business-onlinepayment-description') ) {
2120 my $dtempl = $conf->config('business-onlinepayment-description');
2122 my $agent_obj = $cust_main->agent
2123 or die "can't retreive agent for $cust_main (agentnum ".
2124 $cust_main->agentnum. ")";
2125 my $agent = $agent_obj->agent;
2126 my $pkgs = join(', ',
2127 map { $_->part_pkg->pkg }
2128 grep { $_->pkgnum } $self->cust_bill_pkg
2130 $description = eval qq("$dtempl");
2133 $cust_main->realtime_bop($method, $amount,
2134 'description' => $description,
2135 'invnum' => $self->invnum,
2136 #this didn't do what we want, it just calls apply_payments_and_credits
2138 'apply_to_invoice' => 1,
2141 #this changes application behavior: auto payments
2142 #triggered against a specific invoice are now applied
2143 #to that invoice instead of oldest open.
2149 =item batch_card OPTION => VALUE...
2151 Adds a payment for this invoice to the pending credit card batch (see
2152 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2153 runs the payment using a realtime gateway.
2158 my ($self, %options) = @_;
2159 my $cust_main = $self->cust_main;
2161 $options{invnum} = $self->invnum;
2163 $cust_main->batch_card(%options);
2166 sub _agent_template {
2168 $self->cust_main->agent_template;
2171 sub _agent_invoice_from {
2173 $self->cust_main->agent_invoice_from;
2176 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2178 Returns an text invoice, as a list of lines.
2180 Options can be passed as a hashref (recommended) or as a list of time, template
2181 and then any key/value pairs for any other options.
2183 I<time>, if specified, is used to control the printing of overdue messages. The
2184 default is now. It isn't the date of the invoice; that's the `_date' field.
2185 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2186 L<Time::Local> and L<Date::Parse> for conversion functions.
2188 I<template>, if specified, is the name of a suffix for alternate invoices.
2190 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2196 my( $today, $template, %opt );
2198 %opt = %{ shift() };
2199 $today = delete($opt{'time'}) || '';
2200 $template = delete($opt{template}) || '';
2202 ( $today, $template, %opt ) = @_;
2205 my %params = ( 'format' => 'template' );
2206 $params{'time'} = $today if $today;
2207 $params{'template'} = $template if $template;
2208 $params{$_} = $opt{$_}
2209 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2211 $self->print_generic( %params );
2214 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2216 Internal method - returns a filename of a filled-in LaTeX template for this
2217 invoice (Note: add ".tex" to get the actual filename), and a filename of
2218 an associated logo (with the .eps extension included).
2220 See print_ps and print_pdf for methods that return PostScript and PDF output.
2222 Options can be passed as a hashref (recommended) or as a list of time, template
2223 and then any key/value pairs for any other options.
2225 I<time>, if specified, is used to control the printing of overdue messages. The
2226 default is now. It isn't the date of the invoice; that's the `_date' field.
2227 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2228 L<Time::Local> and L<Date::Parse> for conversion functions.
2230 I<template>, if specified, is the name of a suffix for alternate invoices.
2232 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2238 my $conf = $self->conf;
2239 my( $today, $template, %opt );
2241 %opt = %{ shift() };
2242 $today = delete($opt{'time'}) || '';
2243 $template = delete($opt{template}) || '';
2245 ( $today, $template, %opt ) = @_;
2248 my %params = ( 'format' => 'latex' );
2249 $params{'time'} = $today if $today;
2250 $params{'template'} = $template if $template;
2251 $params{$_} = $opt{$_}
2252 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2254 $template ||= $self->_agent_template;
2256 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2257 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2261 ) or die "can't open temp file: $!\n";
2263 my $agentnum = $self->cust_main->agentnum;
2265 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2266 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2267 or die "can't write temp file: $!\n";
2269 print $lh $conf->config_binary('logo.eps', $agentnum)
2270 or die "can't write temp file: $!\n";
2273 $params{'logo_file'} = $lh->filename;
2275 if($conf->exists('invoice-barcode')){
2276 my $png_file = $self->invoice_barcode($dir);
2277 my $eps_file = $png_file;
2278 $eps_file =~ s/\.png$/.eps/g;
2279 $png_file =~ /(barcode.*png)/;
2281 $eps_file =~ /(barcode.*eps)/;
2284 my $curr_dir = cwd();
2286 # after painfuly long experimentation, it was determined that sam2p won't
2287 # accept : and other chars in the path, no matter how hard I tried to
2288 # escape them, hence the chdir (and chdir back, just to be safe)
2289 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2290 or die "sam2p failed: $!\n";
2294 $params{'barcode_file'} = $eps_file;
2297 my @filled_in = $self->print_generic( %params );
2299 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2303 ) or die "can't open temp file: $!\n";
2304 binmode($fh, ':utf8'); # language support
2305 print $fh join('', @filled_in );
2308 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2309 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2313 =item invoice_barcode DIR_OR_FALSE
2315 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2316 it is taken as the temp directory where the PNG file will be generated and the
2317 PNG file name is returned. Otherwise, the PNG image itself is returned.
2321 sub invoice_barcode {
2322 my ($self, $dir) = (shift,shift);
2324 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2325 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2326 my $gd = $gdbar->plot(Height => 30);
2329 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2333 ) or die "can't open temp file: $!\n";
2334 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2335 my $png_file = $bh->filename;
2342 =item print_generic OPTION => VALUE ...
2344 Internal method - returns a filled-in template for this invoice as a scalar.
2346 See print_ps and print_pdf for methods that return PostScript and PDF output.
2348 Non optional options include
2349 format - latex, html, template
2351 Optional options include
2353 template - a value used as a suffix for a configuration template
2355 time - a value used to control the printing of overdue messages. The
2356 default is now. It isn't the date of the invoice; that's the `_date' field.
2357 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2358 L<Time::Local> and L<Date::Parse> for conversion functions.
2362 unsquelch_cdr - overrides any per customer cdr squelching when true
2364 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2368 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2369 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2370 # yes: fixed width (dot matrix) text printing will be borked
2372 my( $self, %params ) = @_;
2373 my $conf = $self->conf;
2374 my $today = $params{today} ? $params{today} : time;
2375 warn "$me print_generic called on $self with suffix $params{template}\n"
2378 my $format = $params{format};
2379 die "Unknown format: $format"
2380 unless $format =~ /^(latex|html|template)$/;
2382 my $cust_main = $self->cust_main;
2383 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2384 unless $cust_main->payname
2385 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2387 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2388 'html' => [ '<%=', '%>' ],
2389 'template' => [ '{', '}' ],
2392 warn "$me print_generic creating template\n"
2395 #create the template
2396 my $template = $params{template} ? $params{template} : $self->_agent_template;
2397 my $templatefile = "invoice_$format";
2398 $templatefile .= "_$template"
2399 if length($template) && $conf->exists($templatefile."_$template");
2400 my @invoice_template = map "$_\n", $conf->config($templatefile)
2401 or die "cannot load config data $templatefile";
2404 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2405 #change this to a die when the old code is removed
2406 warn "old-style invoice template $templatefile; ".
2407 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2408 $old_latex = 'true';
2409 @invoice_template = _translate_old_latex_format(@invoice_template);
2412 warn "$me print_generic creating T:T object\n"
2415 my $text_template = new Text::Template(
2417 SOURCE => \@invoice_template,
2418 DELIMITERS => $delimiters{$format},
2421 warn "$me print_generic compiling T:T object\n"
2424 $text_template->compile()
2425 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2428 # additional substitution could possibly cause breakage in existing templates
2429 my %convert_maps = (
2431 'notes' => sub { map "$_", @_ },
2432 'footer' => sub { map "$_", @_ },
2433 'smallfooter' => sub { map "$_", @_ },
2434 'returnaddress' => sub { map "$_", @_ },
2435 'coupon' => sub { map "$_", @_ },
2436 'summary' => sub { map "$_", @_ },
2442 s/%%(.*)$/<!-- $1 -->/g;
2443 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2444 s/\\begin\{enumerate\}/<ol>/g;
2446 s/\\end\{enumerate\}/<\/ol>/g;
2447 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2456 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2458 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2463 s/\\\\\*?\s*$/<BR>/;
2464 s/\\hyphenation\{[\w\s\-]+}//;
2469 'coupon' => sub { "" },
2470 'summary' => sub { "" },
2477 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2478 s/\\begin\{enumerate\}//g;
2480 s/\\end\{enumerate\}//g;
2481 s/\\textbf\{(.*)\}/$1/g;
2488 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2490 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2495 s/\\\\\*?\s*$/\n/; # dubious
2496 s/\\hyphenation\{[\w\s\-]+}//;
2500 'coupon' => sub { "" },
2501 'summary' => sub { "" },
2506 # hashes for differing output formats
2507 my %nbsps = ( 'latex' => '~',
2508 'html' => '', # '&nbps;' would be nice
2509 'template' => '', # not used
2511 my $nbsp = $nbsps{$format};
2513 my %escape_functions = ( 'latex' => \&_latex_escape,
2514 'html' => \&_html_escape_nbsp,#\&encode_entities,
2515 'template' => sub { shift },
2517 my $escape_function = $escape_functions{$format};
2518 my $escape_function_nonbsp = ($format eq 'html')
2519 ? \&_html_escape : $escape_function;
2521 my %date_formats = ( 'latex' => $date_format_long,
2522 'html' => $date_format_long,
2525 $date_formats{'html'} =~ s/ / /g;
2527 my $date_format = $date_formats{$format};
2529 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2531 'html' => sub { return '<b>'. shift(). '</b>'
2533 'template' => sub { shift },
2535 my $embolden_function = $embolden_functions{$format};
2537 my %newline_tokens = ( 'latex' => '\\\\',
2541 my $newline_token = $newline_tokens{$format};
2543 warn "$me generating template variables\n"
2546 # generate template variables
2549 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2553 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2559 $returnaddress = join("\n",
2560 $conf->config_orbase("invoice_${format}returnaddress", $template)
2563 } elsif ( grep /\S/,
2564 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2566 my $convert_map = $convert_maps{$format}{'returnaddress'};
2569 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2574 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2576 my $convert_map = $convert_maps{$format}{'returnaddress'};
2577 $returnaddress = join( "\n", &$convert_map(
2578 map { s/( {2,})/'~' x length($1)/eg;
2582 ( $conf->config('company_name', $self->cust_main->agentnum),
2583 $conf->config('company_address', $self->cust_main->agentnum),
2590 my $warning = "Couldn't find a return address; ".
2591 "do you need to set the company_address configuration value?";
2593 $returnaddress = $nbsp;
2594 #$returnaddress = $warning;
2598 warn "$me generating invoice data\n"
2601 my $agentnum = $self->cust_main->agentnum;
2603 my %invoice_data = (
2606 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2607 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2608 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2609 'returnaddress' => $returnaddress,
2610 'agent' => &$escape_function($cust_main->agent->agent),
2613 'invnum' => $self->invnum,
2614 'date' => time2str($date_format, $self->_date),
2615 'today' => time2str($date_format_long, $today),
2616 'terms' => $self->terms,
2617 'template' => $template, #params{'template'},
2618 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2619 'current_charges' => sprintf("%.2f", $self->charged),
2620 'duedate' => $self->due_date2str($rdate_format), #date_format?
2623 'custnum' => $cust_main->display_custnum,
2624 'agent_custid' => &$escape_function($cust_main->agent_custid),
2625 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2626 payname company address1 address2 city state zip fax
2630 'ship_enable' => $conf->exists('invoice-ship_address'),
2631 'unitprices' => $conf->exists('invoice-unitprice'),
2632 'smallernotes' => $conf->exists('invoice-smallernotes'),
2633 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2634 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2636 #layout info -- would be fancy to calc some of this and bury the template
2638 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2639 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2640 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2641 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2642 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2643 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2644 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2645 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2646 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2647 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2649 # better hang on to conf_dir for a while (for old templates)
2650 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2652 #these are only used when doing paged plaintext
2659 my $lh = FS::L10N->get_handle($cust_main->locale);
2660 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2661 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2662 # eval to avoid death for unimplemented languages
2663 my $dh = eval { Date::Language->new($info{'name'}) } ||
2664 Date::Language->new(); # fall back to English
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' => $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
2869 'summarized' => $summarypage ? 'Y' : '',
2871 my $tax_weight = _pkg_category($tax_section->{description})
2872 ? _pkg_category($tax_section->{description})->weight
2874 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2875 $tax_section->{'sort_weight'} = $tax_weight;
2878 my $adjusttotal = 0;
2879 my $adjust_section = { 'description' =>
2880 $self->mt('Credits, Payments, and Adjustments'),
2881 'subtotal' => 0, # adjusted below
2882 'summarized' => $summarypage ? 'Y' : '',
2884 my $adjust_weight = _pkg_category($adjust_section->{description})
2885 ? _pkg_category($adjust_section->{description})->weight
2887 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2888 $adjust_section->{'sort_weight'} = $adjust_weight;
2890 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2891 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2892 $invoice_data{'multisection'} = $multisection;
2893 my $late_sections = [];
2894 my $extra_sections = [];
2895 my $extra_lines = ();
2896 if ( $multisection ) {
2897 ($extra_sections, $extra_lines) =
2898 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2899 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2901 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2903 push @detail_items, @$extra_lines if $extra_lines;
2905 $self->_items_sections( $late_sections, # this could stand a refactor
2907 $escape_function_nonbsp,
2911 if ($conf->exists('svc_phone_sections')) {
2912 my ($phone_sections, $phone_lines) =
2913 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2914 push @{$late_sections}, @$phone_sections;
2915 push @detail_items, @$phone_lines;
2917 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2918 my ($accountcode_section, $accountcode_lines) =
2919 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2920 if ( scalar(@$accountcode_lines) ) {
2921 push @{$late_sections}, $accountcode_section;
2922 push @detail_items, @$accountcode_lines;
2925 } else {# not multisection
2926 # make a default section
2927 push @sections, { 'description' => '', 'subtotal' => '' };
2928 # and calculate the finance charge total, since it won't get done otherwise.
2929 # XXX possibly other totals?
2930 # XXX possibly finance_pkgclass should not be used in this manner?
2931 if ( $conf->exists('finance_pkgclass') ) {
2932 my @finance_charges;
2933 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2934 if ( grep { $_->section eq $invoice_data{finance_section} }
2935 $cust_bill_pkg->cust_bill_pkg_display ) {
2936 # I think these are always setup fees, but just to be sure...
2937 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2940 $invoice_data{finance_amount} =
2941 sprintf('%.2f', sum( @finance_charges ) || 0);
2945 unless ( $conf->exists('disable_previous_balance')
2946 || $conf->exists('previous_balance-summary_only')
2950 warn "$me adding previous balances\n"
2953 foreach my $line_item ( $self->_items_previous ) {
2956 ext_description => [],
2958 $detail->{'ref'} = $line_item->{'pkgnum'};
2959 $detail->{'quantity'} = 1;
2960 $detail->{'section'} = $previous_section;
2961 $detail->{'description'} = &$escape_function($line_item->{'description'});
2962 if ( exists $line_item->{'ext_description'} ) {
2963 @{$detail->{'ext_description'}} = map {
2964 &$escape_function($_);
2965 } @{$line_item->{'ext_description'}};
2967 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2968 $line_item->{'amount'};
2969 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2971 push @detail_items, $detail;
2972 push @buf, [ $detail->{'description'},
2973 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2979 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2980 push @buf, ['','-----------'];
2981 push @buf, [ $self->mt('Total Previous Balance'),
2982 $money_char. sprintf("%10.2f", $pr_total) ];
2986 if ( $conf->exists('svc_phone-did-summary') ) {
2987 warn "$me adding DID summary\n"
2990 my ($didsummary,$minutes) = $self->_did_summary;
2991 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2993 { 'description' => $didsummary_desc,
2994 'ext_description' => [ $didsummary, $minutes ],
2998 foreach my $section (@sections, @$late_sections) {
3000 warn "$me adding section \n". Dumper($section)
3003 # begin some normalization
3004 $section->{'subtotal'} = $section->{'amount'}
3006 && !exists($section->{subtotal})
3007 && exists($section->{amount});
3009 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3010 if ( $invoice_data{finance_section} &&
3011 $section->{'description'} eq $invoice_data{finance_section} );
3013 $section->{'subtotal'} = $other_money_char.
3014 sprintf('%.2f', $section->{'subtotal'})
3017 # continue some normalization
3018 $section->{'amount'} = $section->{'subtotal'}
3022 if ( $section->{'description'} ) {
3023 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3028 warn "$me setting options\n"
3031 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3033 $options{'section'} = $section if $multisection;
3034 $options{'format'} = $format;
3035 $options{'escape_function'} = $escape_function;
3036 $options{'format_function'} = sub { () } unless $unsquelched;
3037 $options{'unsquelched'} = $unsquelched;
3038 $options{'summary_page'} = $summarypage;
3039 $options{'skip_usage'} =
3040 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3041 $options{'multilocation'} = $multilocation;
3042 $options{'multisection'} = $multisection;
3044 warn "$me searching for line items\n"
3047 foreach my $line_item ( $self->_items_pkg(%options) ) {
3049 warn "$me adding line item $line_item\n"
3053 ext_description => [],
3055 $detail->{'ref'} = $line_item->{'pkgnum'};
3056 $detail->{'quantity'} = $line_item->{'quantity'};
3057 $detail->{'section'} = $section;
3058 $detail->{'description'} = &$escape_function($line_item->{'description'});
3059 if ( exists $line_item->{'ext_description'} ) {
3060 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3062 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3063 $line_item->{'amount'};
3064 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3065 $line_item->{'unit_amount'};
3066 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3068 $detail->{'sdate'} = $line_item->{'sdate'};
3069 $detail->{'edate'} = $line_item->{'edate'};
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};
3271 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3272 $total->{'total_amount'} =
3273 &$embolden_function(
3274 $other_money_char. sprintf('%.2f', $summarypage
3276 $self->billing_balance
3277 : $self->owed + $pr_total
3280 if ( $multisection && !$adjust_section->{sort_weight} ) {
3281 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3282 $total->{'total_amount'};
3284 push @total_items, $total;
3286 push @buf,['','-----------'];
3287 push @buf,[$self->balance_due_msg, $money_char.
3288 sprintf("%10.2f", $balance_due ) ];
3291 if ( $conf->exists('previous_balance-show_credit')
3292 and $cust_main->balance < 0 ) {
3293 my $credit_total = {
3294 'total_item' => &$embolden_function($self->credit_balance_msg),
3295 'total_amount' => &$embolden_function(
3296 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3299 if ( $multisection ) {
3300 $adjust_section->{'posttotal'} .= $newline_token .
3301 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3304 push @total_items, $credit_total;
3306 push @buf,['','-----------'];
3307 push @buf,[$self->credit_balance_msg, $money_char.
3308 sprintf("%10.2f", -$cust_main->balance ) ];
3312 if ( $multisection ) {
3313 if ($conf->exists('svc_phone_sections')) {
3315 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3316 $total->{'total_amount'} =
3317 &$embolden_function(
3318 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3320 my $last_section = pop @sections;
3321 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3322 $total->{'total_amount'};
3323 push @sections, $last_section;
3325 push @sections, @$late_sections
3329 my @includelist = ();
3330 push @includelist, 'summary' if $summarypage;
3331 foreach my $include ( @includelist ) {
3333 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3336 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3338 @inc_src = $conf->config($inc_file, $agentnum);
3342 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3344 my $convert_map = $convert_maps{$format}{$include};
3346 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3347 s/--\@\]/$delimiters{$format}[1]/g;
3350 &$convert_map( $conf->config($inc_file, $agentnum) );
3354 my $inc_tt = new Text::Template (
3356 SOURCE => [ map "$_\n", @inc_src ],
3357 DELIMITERS => $delimiters{$format},
3358 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3360 unless ( $inc_tt->compile() ) {
3361 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3362 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3366 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3368 $invoice_data{$include} =~ s/\n+$//
3369 if ($format eq 'latex');
3374 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3375 /invoice_lines\((\d*)\)/;
3376 $invoice_lines += $1 || scalar(@buf);
3379 die "no invoice_lines() functions in template?"
3380 if ( $format eq 'template' && !$wasfunc );
3382 if ($format eq 'template') {
3384 if ( $invoice_lines ) {
3385 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3386 $invoice_data{'total_pages'}++
3387 if scalar(@buf) % $invoice_lines;
3390 #setup subroutine for the template
3391 #sub FS::cust_bill::_template::invoice_lines { # good god, no
3392 $invoice_data{invoice_lines} = sub { # much better
3393 my $lines = shift || scalar(@buf);
3405 push @collect, split("\n",
3406 $text_template->fill_in( HASH => \%invoice_data )
3408 $invoice_data{'page'}++;
3410 map "$_\n", @collect;
3412 # this is where we actually create the invoice
3413 warn "filling in template for invoice ". $self->invnum. "\n"
3415 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3418 $text_template->fill_in(HASH => \%invoice_data);
3422 # helper routine for generating date ranges
3423 sub _prior_month30s {
3426 [ 1, 2592000 ], # 0-30 days ago
3427 [ 2592000, 5184000 ], # 30-60 days ago
3428 [ 5184000, 7776000 ], # 60-90 days ago
3429 [ 7776000, 0 ], # 90+ days ago
3432 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3433 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3438 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3440 Returns an postscript invoice, as a scalar.
3442 Options can be passed as a hashref (recommended) or as a list of time, template
3443 and then any key/value pairs for any other options.
3445 I<time> an optional value used to control the printing of overdue messages. The
3446 default is now. It isn't the date of the invoice; that's the `_date' field.
3447 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3448 L<Time::Local> and L<Date::Parse> for conversion functions.
3450 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3457 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3458 my $ps = generate_ps($file);
3460 unlink($barcodefile) if $barcodefile;
3465 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3467 Returns an PDF invoice, as a scalar.
3469 Options can be passed as a hashref (recommended) or as a list of time, template
3470 and then any key/value pairs for any other options.
3472 I<time> an optional value used to control the printing of overdue messages. The
3473 default is now. It isn't the date of the invoice; that's the `_date' field.
3474 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3475 L<Time::Local> and L<Date::Parse> for conversion functions.
3477 I<template>, if specified, is the name of a suffix for alternate invoices.
3479 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3486 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3487 my $pdf = generate_pdf($file);
3489 unlink($barcodefile) if $barcodefile;
3494 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3496 Returns an HTML invoice, as a scalar.
3498 I<time> an optional value used to control the printing of overdue messages. The
3499 default is now. It isn't the date of the invoice; that's the `_date' field.
3500 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3501 L<Time::Local> and L<Date::Parse> for conversion functions.
3503 I<template>, if specified, is the name of a suffix for alternate invoices.
3505 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3507 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3508 when emailing the invoice as part of a multipart/related MIME email.
3516 %params = %{ shift() };
3518 $params{'time'} = shift;
3519 $params{'template'} = shift;
3520 $params{'cid'} = shift;
3523 $params{'format'} = 'html';
3525 $self->print_generic( %params );
3528 # quick subroutine for print_latex
3530 # There are ten characters that LaTeX treats as special characters, which
3531 # means that they do not simply typeset themselves:
3532 # # $ % & ~ _ ^ \ { }
3534 # TeX ignores blanks following an escaped character; if you want a blank (as
3535 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3539 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3540 $value =~ s/([<>])/\$$1\$/g;
3546 encode_entities($value);
3550 sub _html_escape_nbsp {
3551 my $value = _html_escape(shift);
3552 $value =~ s/ +/ /g;
3556 #utility methods for print_*
3558 sub _translate_old_latex_format {
3559 warn "_translate_old_latex_format called\n"
3566 if ( $line =~ /^%%Detail\s*$/ ) {
3568 push @template, q![@--!,
3569 q! foreach my $_tr_line (@detail_items) {!,
3570 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3571 q! $_tr_line->{'description'} .= !,
3572 q! "\\tabularnewline\n~~".!,
3573 q! join( "\\tabularnewline\n~~",!,
3574 q! @{$_tr_line->{'ext_description'}}!,
3578 while ( ( my $line_item_line = shift )
3579 !~ /^%%EndDetail\s*$/ ) {
3580 $line_item_line =~ s/'/\\'/g; # nice LTS
3581 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3582 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3583 push @template, " \$OUT .= '$line_item_line';";
3586 push @template, '}',
3589 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3591 push @template, '[@--',
3592 ' foreach my $_tr_line (@total_items) {';
3594 while ( ( my $total_item_line = shift )
3595 !~ /^%%EndTotalDetails\s*$/ ) {
3596 $total_item_line =~ s/'/\\'/g; # nice LTS
3597 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3598 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3599 push @template, " \$OUT .= '$total_item_line';";
3602 push @template, '}',
3606 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3607 push @template, $line;
3613 warn "$_\n" foreach @template;
3621 my $conf = $self->conf;
3623 #check for an invoice-specific override
3624 return $self->invoice_terms if $self->invoice_terms;
3626 #check for a customer- specific override
3627 my $cust_main = $self->cust_main;
3628 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3630 #use configured default
3631 $conf->config('invoice_default_terms') || '';
3637 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3638 $duedate = $self->_date() + ( $1 * 86400 );
3645 $self->due_date ? time2str(shift, $self->due_date) : '';
3648 sub balance_due_msg {
3650 my $msg = $self->mt('Balance Due');
3651 return $msg unless $self->terms;
3652 if ( $self->due_date ) {
3653 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3654 $self->due_date2str($date_format);
3655 } elsif ( $self->terms ) {
3656 $msg .= ' - '. $self->terms;
3661 sub balance_due_date {
3663 my $conf = $self->conf;
3665 if ( $conf->exists('invoice_default_terms')
3666 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3667 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3672 sub credit_balance_msg {
3674 $self->mt('Credit Balance Remaining')
3677 =item invnum_date_pretty
3679 Returns a string with the invoice number and date, for example:
3680 "Invoice #54 (3/20/2008)"
3684 sub invnum_date_pretty {
3686 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3691 Returns a string with the date, for example: "3/20/2008"
3697 time2str($date_format, $self->_date);
3700 # I like how _date_pretty was documented but this one wasn't.
3702 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3704 Generate section information for all items appearing on this invoice.
3705 This will only be called for multi-section invoices.
3707 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3708 related display records (L<FS::cust_bill_pkg_display>) and organize
3709 them into two groups ("early" and "late" according to whether they come
3710 before or after the total), then into sections. A subtotal is calculated
3713 Section descriptions are returned in sort weight order. Each consists
3714 of a hash containing:
3716 description: the package category name, escaped
3717 subtotal: the total charges in that section
3718 tax_section: a flag indicating that the section contains only tax charges
3719 summarized: same as tax_section, for some reason
3720 sort_weight: the package category's sort weight
3722 If 'condense' is set on the display record, it also contains everything
3723 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3724 coderefs to generate parts of the invoice. This is not advised.
3726 Takes way too many arguments, all mandatory:
3728 LATE: an arrayref to push the "late" section hashes onto. The "early"
3729 group is simply returned from the method. Yes, I know. Don't ask.
3731 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3732 Turning this on has the following effects:
3733 - Ignores display items with the 'summary' flag.
3734 - Combines all items into the "early" group.
3735 - Creates sections for all non-disabled package categories, even if they
3736 have no charges on this invoice, as well as a section with no name.
3738 ESCAPE: an escape function to use for section titles. Why not just
3739 let the calling environment escape things itself? Beats the heck out
3742 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3743 sorted list. If there are any of these, section subtotals exclude
3746 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3747 passed through to C<_condense_section()>.
3751 use vars qw(%pkg_category_cache);
3752 sub _items_sections {
3755 my $summarypage = shift;
3757 my $extra_sections = shift;
3761 my %late_subtotal = ();
3764 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3767 my $usage = $cust_bill_pkg->usage;
3769 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3770 next if ( $display->summary && $summarypage );
3772 my $section = $display->section;
3773 my $type = $display->type;
3775 $not_tax{$section} = 1
3776 unless $cust_bill_pkg->pkgnum == 0;
3778 if ( $display->post_total && !$summarypage ) {
3779 if (! $type || $type eq 'S') {
3780 $late_subtotal{$section} += $cust_bill_pkg->setup
3781 if $cust_bill_pkg->setup != 0;
3785 $late_subtotal{$section} += $cust_bill_pkg->recur
3786 if $cust_bill_pkg->recur != 0;
3789 if ($type && $type eq 'R') {
3790 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3791 if $cust_bill_pkg->recur != 0;
3794 if ($type && $type eq 'U') {
3795 $late_subtotal{$section} += $usage
3796 unless scalar(@$extra_sections);
3801 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3803 if (! $type || $type eq 'S') {
3804 $subtotal{$section} += $cust_bill_pkg->setup
3805 if $cust_bill_pkg->setup != 0;
3809 $subtotal{$section} += $cust_bill_pkg->recur
3810 if $cust_bill_pkg->recur != 0;
3813 if ($type && $type eq 'R') {
3814 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3815 if $cust_bill_pkg->recur != 0;
3818 if ($type && $type eq 'U') {
3819 $subtotal{$section} += $usage
3820 unless scalar(@$extra_sections);
3829 %pkg_category_cache = ();
3831 push @$late, map { { 'description' => &{$escape}($_),
3832 'subtotal' => $late_subtotal{$_},
3834 'sort_weight' => ( _pkg_category($_)
3835 ? _pkg_category($_)->weight
3838 ((_pkg_category($_) && _pkg_category($_)->condense)
3839 ? $self->_condense_section($format)
3843 sort _sectionsort keys %late_subtotal;
3846 if ( $summarypage ) {
3847 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3848 map { $_->categoryname } qsearch('pkg_category', {});
3849 push @sections, '' if exists($subtotal{''});
3851 @sections = keys %subtotal;
3854 my @early = map { { 'description' => &{$escape}($_),
3855 'subtotal' => $subtotal{$_},
3856 'summarized' => $not_tax{$_} ? '' : 'Y',
3857 'tax_section' => $not_tax{$_} ? '' : 'Y',
3858 'sort_weight' => ( _pkg_category($_)
3859 ? _pkg_category($_)->weight
3862 ((_pkg_category($_) && _pkg_category($_)->condense)
3863 ? $self->_condense_section($format)
3868 push @early, @$extra_sections if $extra_sections;
3870 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3874 #helper subs for above
3877 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3881 my $categoryname = shift;
3882 $pkg_category_cache{$categoryname} ||=
3883 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3886 my %condensed_format = (
3887 'label' => [ qw( Description Qty Amount ) ],
3889 sub { shift->{description} },
3890 sub { shift->{quantity} },
3891 sub { my($href, %opt) = @_;
3892 ($opt{dollar} || ''). $href->{amount};
3895 'align' => [ qw( l r r ) ],
3896 'span' => [ qw( 5 1 1 ) ], # unitprices?
3897 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3900 sub _condense_section {
3901 my ( $self, $format ) = ( shift, shift );
3903 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3904 qw( description_generator
3907 total_line_generator
3912 sub _condensed_generator_defaults {
3913 my ( $self, $format ) = ( shift, shift );
3914 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3923 sub _condensed_header_generator {
3924 my ( $self, $format ) = ( shift, shift );
3926 my ( $f, $prefix, $suffix, $separator, $column ) =
3927 _condensed_generator_defaults($format);
3929 if ($format eq 'latex') {
3930 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3931 $suffix = "\\\\\n\\hline";
3934 sub { my ($d,$a,$s,$w) = @_;
3935 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3937 } elsif ( $format eq 'html' ) {
3938 $prefix = '<th></th>';
3942 sub { my ($d,$a,$s,$w) = @_;
3943 return qq!<th align="$html_align{$a}">$d</th>!;
3951 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3953 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3956 $prefix. join($separator, @result). $suffix;
3961 sub _condensed_description_generator {
3962 my ( $self, $format ) = ( shift, shift );
3964 my ( $f, $prefix, $suffix, $separator, $column ) =
3965 _condensed_generator_defaults($format);
3967 my $money_char = '$';
3968 if ($format eq 'latex') {
3969 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3971 $separator = " & \n";
3973 sub { my ($d,$a,$s,$w) = @_;
3974 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3976 $money_char = '\\dollar';
3977 }elsif ( $format eq 'html' ) {
3978 $prefix = '"><td align="center"></td>';
3982 sub { my ($d,$a,$s,$w) = @_;
3983 return qq!<td align="$html_align{$a}">$d</td>!;
3985 #$money_char = $conf->config('money_char') || '$';
3986 $money_char = ''; # this is madness
3994 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3996 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3998 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3999 map { $f->{$_}->[$i] } qw(align span width)
4003 $prefix. join( $separator, @result ). $suffix;
4008 sub _condensed_total_generator {
4009 my ( $self, $format ) = ( shift, shift );
4011 my ( $f, $prefix, $suffix, $separator, $column ) =
4012 _condensed_generator_defaults($format);
4015 if ($format eq 'latex') {
4018 $separator = " & \n";
4020 sub { my ($d,$a,$s,$w) = @_;
4021 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4023 }elsif ( $format eq 'html' ) {
4027 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4029 sub { my ($d,$a,$s,$w) = @_;
4030 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4039 # my $r = &{$f->{fields}->[$i]}(@args);
4040 # $r .= ' Total' unless $i;
4042 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4044 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4045 map { $f->{$_}->[$i] } qw(align span width)
4049 $prefix. join( $separator, @result ). $suffix;
4054 =item total_line_generator FORMAT
4056 Returns a coderef used for generation of invoice total line items for this
4057 usage_class. FORMAT is either html or latex
4061 # should not be used: will have issues with hash element names (description vs
4062 # total_item and amount vs total_amount -- another array of functions?
4064 sub _condensed_total_line_generator {
4065 my ( $self, $format ) = ( shift, shift );
4067 my ( $f, $prefix, $suffix, $separator, $column ) =
4068 _condensed_generator_defaults($format);
4071 if ($format eq 'latex') {
4074 $separator = " & \n";
4076 sub { my ($d,$a,$s,$w) = @_;
4077 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4079 }elsif ( $format eq 'html' ) {
4083 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4085 sub { my ($d,$a,$s,$w) = @_;
4086 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4095 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4097 &{$column}( &{$f->{fields}->[$i]}(@args),
4098 map { $f->{$_}->[$i] } qw(align span width)
4102 $prefix. join( $separator, @result ). $suffix;
4107 #sub _items_extra_usage_sections {
4109 # my $escape = shift;
4111 # my %sections = ();
4113 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4114 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4116 # next unless $cust_bill_pkg->pkgnum > 0;
4118 # foreach my $section ( keys %usage_class ) {
4120 # my $usage = $cust_bill_pkg->usage($section);
4122 # next unless $usage && $usage > 0;
4124 # $sections{$section} ||= 0;
4125 # $sections{$section} += $usage;
4131 # map { { 'description' => &{$escape}($_),
4132 # 'subtotal' => $sections{$_},
4133 # 'summarized' => '',
4134 # 'tax_section' => '',
4137 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4141 sub _items_extra_usage_sections {
4143 my $conf = $self->conf;
4151 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4153 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4154 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4155 next unless $cust_bill_pkg->pkgnum > 0;
4157 foreach my $classnum ( keys %usage_class ) {
4158 my $section = $usage_class{$classnum}->classname;
4159 $classnums{$section} = $classnum;
4161 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4162 my $amount = $detail->amount;
4163 next unless $amount && $amount > 0;
4165 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4166 $sections{$section}{amount} += $amount; #subtotal
4167 $sections{$section}{calls}++;
4168 $sections{$section}{duration} += $detail->duration;
4170 my $desc = $detail->regionname;
4171 my $description = $desc;
4172 $description = substr($desc, 0, $maxlength). '...'
4173 if $format eq 'latex' && length($desc) > $maxlength;
4175 $lines{$section}{$desc} ||= {
4176 description => &{$escape}($description),
4177 #pkgpart => $part_pkg->pkgpart,
4178 pkgnum => $cust_bill_pkg->pkgnum,
4183 #unit_amount => $cust_bill_pkg->unitrecur,
4184 quantity => $cust_bill_pkg->quantity,
4185 product_code => 'N/A',
4186 ext_description => [],
4189 $lines{$section}{$desc}{amount} += $amount;
4190 $lines{$section}{$desc}{calls}++;
4191 $lines{$section}{$desc}{duration} += $detail->duration;
4197 my %sectionmap = ();
4198 foreach (keys %sections) {
4199 my $usage_class = $usage_class{$classnums{$_}};
4200 $sectionmap{$_} = { 'description' => &{$escape}($_),
4201 'amount' => $sections{$_}{amount}, #subtotal
4202 'calls' => $sections{$_}{calls},
4203 'duration' => $sections{$_}{duration},
4205 'tax_section' => '',
4206 'sort_weight' => $usage_class->weight,
4207 ( $usage_class->format
4208 ? ( map { $_ => $usage_class->$_($format) }
4209 qw( description_generator header_generator total_generator total_line_generator )
4216 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4220 foreach my $section ( keys %lines ) {
4221 foreach my $line ( keys %{$lines{$section}} ) {
4222 my $l = $lines{$section}{$line};
4223 $l->{section} = $sectionmap{$section};
4224 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4225 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4230 return(\@sections, \@lines);
4236 my $end = $self->_date;
4238 # start at date of previous invoice + 1 second or 0 if no previous invoice
4239 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4240 $start = 0 if !$start;
4243 my $cust_main = $self->cust_main;
4244 my @pkgs = $cust_main->all_pkgs;
4245 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4248 foreach my $pkg ( @pkgs ) {
4249 my @h_cust_svc = $pkg->h_cust_svc($end);
4250 foreach my $h_cust_svc ( @h_cust_svc ) {
4251 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4252 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4254 my $inserted = $h_cust_svc->date_inserted;
4255 my $deleted = $h_cust_svc->date_deleted;
4256 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4258 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4260 # DID either activated or ported in; cannot be both for same DID simultaneously
4261 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4262 && (!$phone_inserted->lnp_status
4263 || $phone_inserted->lnp_status eq ''
4264 || $phone_inserted->lnp_status eq 'native')) {
4267 else { # this one not so clean, should probably move to (h_)svc_phone
4268 my $phone_portedin = qsearchs( 'h_svc_phone',
4269 { 'svcnum' => $h_cust_svc->svcnum,
4270 'lnp_status' => 'portedin' },
4271 FS::h_svc_phone->sql_h_searchs($end),
4273 $num_portedin++ if $phone_portedin;
4276 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4277 if($deleted >= $start && $deleted <= $end && $phone_deleted
4278 && (!$phone_deleted->lnp_status
4279 || $phone_deleted->lnp_status ne 'portingout')) {
4282 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4283 && $phone_deleted->lnp_status
4284 && $phone_deleted->lnp_status eq 'portingout') {
4288 # increment usage minutes
4289 if ( $phone_inserted ) {
4290 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4291 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4294 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4297 # don't look at this service again
4298 push @seen, $h_cust_svc->svcnum;
4302 $minutes = sprintf("%d", $minutes);
4303 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4304 . "$num_deactivated Ported-Out: $num_portedout ",
4305 "Total Minutes: $minutes");
4308 sub _items_accountcode_cdr {
4313 my $section = { 'amount' => 0,
4316 'sort_weight' => '',
4318 'description' => 'Usage by Account Code',
4324 my %accountcodes = ();
4326 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4327 next unless $cust_bill_pkg->pkgnum > 0;
4329 my @header = $cust_bill_pkg->details_header;
4330 next unless scalar(@header);
4331 $section->{'header'} = join(',',@header);
4333 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4335 $section->{'header'} = $detail->formatted('format' => $format)
4336 if($detail->detail eq $section->{'header'});
4338 my $accountcode = $detail->accountcode;
4339 next unless $accountcode;
4341 my $amount = $detail->amount;
4342 next unless $amount && $amount > 0;
4344 $accountcodes{$accountcode} ||= {
4345 description => $accountcode,
4352 product_code => 'N/A',
4353 section => $section,
4354 ext_description => [ $section->{'header'} ],
4358 $section->{'amount'} += $amount;
4359 $accountcodes{$accountcode}{'amount'} += $amount;
4360 $accountcodes{$accountcode}{calls}++;
4361 $accountcodes{$accountcode}{duration} += $detail->duration;
4362 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4366 foreach my $l ( values %accountcodes ) {
4367 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4368 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4369 foreach my $sorted_detail ( @sorted_detail ) {
4370 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4372 delete $l->{detail_temp};
4376 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4378 return ($section,\@sorted_lines);
4381 sub _items_svc_phone_sections {
4383 my $conf = $self->conf;
4391 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4393 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4394 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4396 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4397 next unless $cust_bill_pkg->pkgnum > 0;
4399 my @header = $cust_bill_pkg->details_header;
4400 next unless scalar(@header);
4402 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4404 my $phonenum = $detail->phonenum;
4405 next unless $phonenum;
4407 my $amount = $detail->amount;
4408 next unless $amount && $amount > 0;
4410 $sections{$phonenum} ||= { 'amount' => 0,
4413 'sort_weight' => -1,
4414 'phonenum' => $phonenum,
4416 $sections{$phonenum}{amount} += $amount; #subtotal
4417 $sections{$phonenum}{calls}++;
4418 $sections{$phonenum}{duration} += $detail->duration;
4420 my $desc = $detail->regionname;
4421 my $description = $desc;
4422 $description = substr($desc, 0, $maxlength). '...'
4423 if $format eq 'latex' && length($desc) > $maxlength;
4425 $lines{$phonenum}{$desc} ||= {
4426 description => &{$escape}($description),
4427 #pkgpart => $part_pkg->pkgpart,
4435 product_code => 'N/A',
4436 ext_description => [],
4439 $lines{$phonenum}{$desc}{amount} += $amount;
4440 $lines{$phonenum}{$desc}{calls}++;
4441 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4443 my $line = $usage_class{$detail->classnum}->classname;
4444 $sections{"$phonenum $line"} ||=
4448 'sort_weight' => $usage_class{$detail->classnum}->weight,
4449 'phonenum' => $phonenum,
4450 'header' => [ @header ],
4452 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4453 $sections{"$phonenum $line"}{calls}++;
4454 $sections{"$phonenum $line"}{duration} += $detail->duration;
4456 $lines{"$phonenum $line"}{$desc} ||= {
4457 description => &{$escape}($description),
4458 #pkgpart => $part_pkg->pkgpart,
4466 product_code => 'N/A',
4467 ext_description => [],
4470 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4471 $lines{"$phonenum $line"}{$desc}{calls}++;
4472 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4473 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4474 $detail->formatted('format' => $format);
4479 my %sectionmap = ();
4480 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4481 foreach ( keys %sections ) {
4482 my @header = @{ $sections{$_}{header} || [] };
4484 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4485 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4486 my $usage_class = $summary ? $simple : $usage_simple;
4487 my $ending = $summary ? ' usage charges' : '';
4490 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4492 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4493 'amount' => $sections{$_}{amount}, #subtotal
4494 'calls' => $sections{$_}{calls},
4495 'duration' => $sections{$_}{duration},
4497 'tax_section' => '',
4498 'phonenum' => $sections{$_}{phonenum},
4499 'sort_weight' => $sections{$_}{sort_weight},
4500 'post_total' => $summary, #inspire pagebreak
4502 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4503 qw( description_generator
4506 total_line_generator
4513 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4514 $a->{sort_weight} <=> $b->{sort_weight}
4519 foreach my $section ( keys %lines ) {
4520 foreach my $line ( keys %{$lines{$section}} ) {
4521 my $l = $lines{$section}{$line};
4522 $l->{section} = $sectionmap{$section};
4523 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4524 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4529 if($conf->exists('phone_usage_class_summary')) {
4530 # this only works with Latex
4534 # after this, we'll have only two sections per DID:
4535 # Calls Summary and Calls Detail
4536 foreach my $section ( @sections ) {
4537 if($section->{'post_total'}) {
4538 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4539 $section->{'total_line_generator'} = sub { '' };
4540 $section->{'total_generator'} = sub { '' };
4541 $section->{'header_generator'} = sub { '' };
4542 $section->{'description_generator'} = '';
4543 push @newsections, $section;
4544 my %calls_detail = %$section;
4545 $calls_detail{'post_total'} = '';
4546 $calls_detail{'sort_weight'} = '';
4547 $calls_detail{'description_generator'} = sub { '' };
4548 $calls_detail{'header_generator'} = sub {
4549 return ' & Date/Time & Called Number & Duration & Price'
4550 if $format eq 'latex';
4553 $calls_detail{'description'} = 'Calls Detail: '
4554 . $section->{'phonenum'};
4555 push @newsections, \%calls_detail;
4559 # after this, each usage class is collapsed/summarized into a single
4560 # line under the Calls Summary section
4561 foreach my $newsection ( @newsections ) {
4562 if($newsection->{'post_total'}) { # this means Calls Summary
4563 foreach my $section ( @sections ) {
4564 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4565 && !$section->{'post_total'});
4566 my $newdesc = $section->{'description'};
4567 my $tn = $section->{'phonenum'};
4568 $newdesc =~ s/$tn//g;
4569 my $line = { ext_description => [],
4573 calls => $section->{'calls'},
4574 section => $newsection,
4575 duration => $section->{'duration'},
4576 description => $newdesc,
4577 amount => sprintf("%.2f",$section->{'amount'}),
4578 product_code => 'N/A',
4580 push @newlines, $line;
4585 # after this, Calls Details is populated with all CDRs
4586 foreach my $newsection ( @newsections ) {
4587 if(!$newsection->{'post_total'}) { # this means Calls Details
4588 foreach my $line ( @lines ) {
4589 next unless (scalar(@{$line->{'ext_description'}}) &&
4590 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4592 my @extdesc = @{$line->{'ext_description'}};
4594 foreach my $extdesc ( @extdesc ) {
4595 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4596 push @newextdesc, $extdesc;
4598 $line->{'ext_description'} = \@newextdesc;
4599 $line->{'section'} = $newsection;
4600 push @newlines, $line;
4605 return(\@newsections, \@newlines);
4608 return(\@sections, \@lines);
4612 sub _items { # seems to be unused
4615 #my @display = scalar(@_)
4617 # : qw( _items_previous _items_pkg );
4618 # #: qw( _items_pkg );
4619 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4620 my @display = qw( _items_previous _items_pkg );
4623 foreach my $display ( @display ) {
4624 push @b, $self->$display(@_);
4629 sub _items_previous {
4631 my $conf = $self->conf;
4632 my $cust_main = $self->cust_main;
4633 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4635 foreach ( @pr_cust_bill ) {
4636 my $date = $conf->exists('invoice_show_prior_due_date')
4637 ? 'due '. $_->due_date2str($date_format)
4638 : time2str($date_format, $_->_date);
4640 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4641 #'pkgpart' => 'N/A',
4643 'amount' => sprintf("%.2f", $_->owed),
4649 # 'description' => 'Previous Balance',
4650 # #'pkgpart' => 'N/A',
4651 # 'pkgnum' => 'N/A',
4652 # 'amount' => sprintf("%10.2f", $pr_total ),
4653 # 'ext_description' => [ map {
4654 # "Invoice ". $_->invnum.
4655 # " (". time2str("%x",$_->_date). ") ".
4656 # sprintf("%10.2f", $_->owed)
4657 # } @pr_cust_bill ],
4662 =item _items_pkg [ OPTIONS ]
4664 Return line item hashes for each package item on this invoice. Nearly
4667 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4669 The only OPTIONS accepted is 'section', which may point to a hashref
4670 with a key named 'condensed', which may have a true value. If it
4671 does, this method tries to merge identical items into items with
4672 'quantity' equal to the number of items (not the sum of their
4673 separate quantities, for some reason).
4681 warn "$me _items_pkg searching for all package line items\n"
4684 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4686 warn "$me _items_pkg filtering line items\n"
4688 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4690 if ($options{section} && $options{section}->{condensed}) {
4692 warn "$me _items_pkg condensing section\n"
4696 local $Storable::canonical = 1;
4697 foreach ( @items ) {
4699 delete $item->{ref};
4700 delete $item->{ext_description};
4701 my $key = freeze($item);
4702 $itemshash{$key} ||= 0;
4703 $itemshash{$key} ++; # += $item->{quantity};
4705 @items = sort { $a->{description} cmp $b->{description} }
4706 map { my $i = thaw($_);
4707 $i->{quantity} = $itemshash{$_};
4709 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4715 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4722 return 0 unless $a->itemdesc cmp $b->itemdesc;
4723 return -1 if $b->itemdesc eq 'Tax';
4724 return 1 if $a->itemdesc eq 'Tax';
4725 return -1 if $b->itemdesc eq 'Other surcharges';
4726 return 1 if $a->itemdesc eq 'Other surcharges';
4727 $a->itemdesc cmp $b->itemdesc;
4732 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4733 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4736 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4738 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4739 list of hashrefs describing the line items they generate on the invoice.
4741 OPTIONS may include:
4743 format: the invoice format.
4745 escape_function: the function used to escape strings.
4747 format_function: the function used to format CDRs.
4749 section: a hashref containing 'description'; if this is present,
4750 cust_bill_pkg_display records not belonging to this section are
4753 multisection: a flag indicating that this is a multisection invoice,
4754 which does something complicated.
4756 multilocation: a flag to display the location label for the package.
4758 Returns a list of hashrefs, each of which may contain:
4760 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4761 ext_description, which is an arrayref of detail lines to show below
4766 sub _items_cust_bill_pkg {
4768 my $conf = $self->conf;
4769 my $cust_bill_pkgs = shift;
4772 my $format = $opt{format} || '';
4773 my $escape_function = $opt{escape_function} || sub { shift };
4774 my $format_function = $opt{format_function} || '';
4775 my $unsquelched = $opt{unsquelched} || ''; #unused
4776 my $section = $opt{section}->{description} if $opt{section};
4777 my $summary_page = $opt{summary_page} || ''; #unused
4778 my $multilocation = $opt{multilocation} || '';
4779 my $multisection = $opt{multisection} || '';
4780 my $discount_show_always = 0;
4782 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4785 my ($s, $r, $u) = ( undef, undef, undef );
4786 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4789 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4790 if ( $_ && !$cust_bill_pkg->hidden ) {
4791 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4792 $_->{amount} =~ s/^\-0\.00$/0.00/;
4793 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4795 if $_->{amount} != 0
4796 || $discount_show_always
4797 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4798 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4804 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4805 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4808 foreach my $display ( grep { defined($section)
4809 ? $_->section eq $section
4812 #grep { !$_->summary || !$summary_page } # bunk!
4813 grep { !$_->summary || $multisection }
4814 $cust_bill_pkg->cust_bill_pkg_display
4818 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4819 $display->billpkgdisplaynum. "\n"
4822 my $type = $display->type;
4824 my $desc = $cust_bill_pkg->desc;
4825 $desc = substr($desc, 0, $maxlength). '...'
4826 if $format eq 'latex' && length($desc) > $maxlength;
4828 my %details_opt = ( 'format' => $format,
4829 'escape_function' => $escape_function,
4830 'format_function' => $format_function,
4833 if ( $cust_bill_pkg->pkgnum > 0 ) {
4835 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4838 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4840 # start/end dates for invoice formats that do nonstandard
4842 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4844 if ( (!$type || $type eq 'S')
4845 && ( $cust_bill_pkg->setup != 0
4846 || $cust_bill_pkg->setup_show_zero
4851 warn "$me _items_cust_bill_pkg adding setup\n"
4854 my $description = $desc;
4855 $description .= ' Setup'
4856 if $cust_bill_pkg->recur != 0
4857 || $discount_show_always
4858 || $cust_bill_pkg->recur_show_zero;
4861 unless ( $cust_pkg->part_pkg->hide_svc_detail
4862 || $cust_bill_pkg->hidden )
4865 push @d, map &{$escape_function}($_),
4866 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4867 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4869 if ( $multilocation ) {
4870 my $loc = $cust_pkg->location_label;
4871 $loc = substr($loc, 0, $maxlength). '...'
4872 if $format eq 'latex' && length($loc) > $maxlength;
4873 push @d, &{$escape_function}($loc);
4878 push @d, $cust_bill_pkg->details(%details_opt)
4879 if $cust_bill_pkg->recur == 0;
4881 if ( $cust_bill_pkg->hidden ) {
4882 $s->{amount} += $cust_bill_pkg->setup;
4883 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4884 push @{ $s->{ext_description} }, @d;
4888 description => $description,
4889 #pkgpart => $part_pkg->pkgpart,
4890 pkgnum => $cust_bill_pkg->pkgnum,
4891 amount => $cust_bill_pkg->setup,
4892 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4893 unit_amount => $cust_bill_pkg->unitsetup,
4894 quantity => $cust_bill_pkg->quantity,
4895 ext_description => \@d,
4901 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4903 $cust_bill_pkg->recur != 0
4904 || $cust_bill_pkg->setup == 0
4905 || $discount_show_always
4906 || $cust_bill_pkg->recur_show_zero
4911 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4914 my $is_summary = $display->summary;
4915 my $description = ($is_summary && $type && $type eq 'U')
4916 ? "Usage charges" : $desc;
4918 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4919 " - ". time2str($date_format, $cust_bill_pkg->edate).
4921 unless $conf->exists('disable_line_item_date_ranges')
4922 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4926 #at least until cust_bill_pkg has "past" ranges in addition to
4927 #the "future" sdate/edate ones... see #3032
4928 my @dates = ( $self->_date );
4929 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4930 push @dates, $prev->sdate if $prev;
4931 push @dates, undef if !$prev;
4933 unless ( $cust_pkg->part_pkg->hide_svc_detail
4934 || $cust_bill_pkg->itemdesc
4935 || $cust_bill_pkg->hidden
4936 || $is_summary && $type && $type eq 'U' )
4939 warn "$me _items_cust_bill_pkg adding service details\n"
4942 push @d, map &{$escape_function}($_),
4943 $cust_pkg->h_labels_short(@dates, 'I')
4944 #$cust_bill_pkg->edate,
4945 #$cust_bill_pkg->sdate)
4946 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4948 warn "$me _items_cust_bill_pkg done adding service details\n"
4951 if ( $multilocation ) {
4952 my $loc = $cust_pkg->location_label;
4953 $loc = substr($loc, 0, $maxlength). '...'
4954 if $format eq 'latex' && length($loc) > $maxlength;
4955 push @d, &{$escape_function}($loc);
4960 unless ( $is_summary ) {
4961 warn "$me _items_cust_bill_pkg adding details\n"
4964 #instead of omitting details entirely in this case (unwanted side
4965 # effects), just omit CDRs
4966 $details_opt{'format_function'} = sub { () }
4967 if $type && $type eq 'R';
4969 push @d, $cust_bill_pkg->details(%details_opt);
4972 warn "$me _items_cust_bill_pkg calculating amount\n"
4977 $amount = $cust_bill_pkg->recur;
4978 } elsif ($type eq 'R') {
4979 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4980 } elsif ($type eq 'U') {
4981 $amount = $cust_bill_pkg->usage;
4984 if ( !$type || $type eq 'R' ) {
4986 warn "$me _items_cust_bill_pkg adding recur\n"
4989 if ( $cust_bill_pkg->hidden ) {
4990 $r->{amount} += $amount;
4991 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4992 push @{ $r->{ext_description} }, @d;
4995 description => $description,
4996 #pkgpart => $part_pkg->pkgpart,
4997 pkgnum => $cust_bill_pkg->pkgnum,
4999 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5000 unit_amount => $cust_bill_pkg->unitrecur,
5001 quantity => $cust_bill_pkg->quantity,
5003 ext_description => \@d,
5007 } else { # $type eq 'U'
5009 warn "$me _items_cust_bill_pkg adding usage\n"
5012 if ( $cust_bill_pkg->hidden ) {
5013 $u->{amount} += $amount;
5014 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5015 push @{ $u->{ext_description} }, @d;
5018 description => $description,
5019 #pkgpart => $part_pkg->pkgpart,
5020 pkgnum => $cust_bill_pkg->pkgnum,
5022 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5023 unit_amount => $cust_bill_pkg->unitrecur,
5024 quantity => $cust_bill_pkg->quantity,
5026 ext_description => \@d,
5031 } # recurring or usage with recurring charge
5033 } else { #pkgnum tax or one-shot line item (??)
5035 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5038 if ( $cust_bill_pkg->setup != 0 ) {
5040 'description' => $desc,
5041 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5044 if ( $cust_bill_pkg->recur != 0 ) {
5046 'description' => "$desc (".
5047 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5048 time2str($date_format, $cust_bill_pkg->edate). ')',
5049 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5057 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5058 && $conf->exists('discount-show-always'));
5062 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5064 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5065 $_->{amount} =~ s/^\-0\.00$/0.00/;
5066 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5068 if $_->{amount} != 0
5069 || $discount_show_always
5070 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5071 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5075 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5082 sub _items_credits {
5083 my( $self, %opt ) = @_;
5084 my $trim_len = $opt{'trim_len'} || 60;
5088 foreach ( $self->cust_credited ) {
5090 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5092 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5093 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5094 $reason = " ($reason) " if $reason;
5097 #'description' => 'Credit ref\#'. $_->crednum.
5098 # " (". time2str("%x",$_->cust_credit->_date) .")".
5100 'description' => $self->mt('Credit applied').' '.
5101 time2str($date_format,$_->cust_credit->_date). $reason,
5102 'amount' => sprintf("%.2f",$_->amount),
5110 sub _items_payments {
5114 #get & print payments
5115 foreach ( $self->cust_bill_pay ) {
5117 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5120 'description' => $self->mt('Payment received').' '.
5121 time2str($date_format,$_->cust_pay->_date ),
5122 'amount' => sprintf("%.2f", $_->amount )
5130 =item call_details [ OPTION => VALUE ... ]
5132 Returns an array of CSV strings representing the call details for this invoice
5133 The only option available is the boolean prepend_billed_number
5138 my ($self, %opt) = @_;
5140 my $format_function = sub { shift };
5142 if ($opt{prepend_billed_number}) {
5143 $format_function = sub {
5147 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5152 my @details = map { $_->details( 'format_function' => $format_function,
5153 'escape_function' => sub{ return() },
5157 $self->cust_bill_pkg;
5158 my $header = $details[0];
5159 ( $header, grep { $_ ne $header } @details );
5169 =item process_reprint
5173 sub process_reprint {
5174 process_re_X('print', @_);
5177 =item process_reemail
5181 sub process_reemail {
5182 process_re_X('email', @_);
5190 process_re_X('fax', @_);
5198 process_re_X('ftp', @_);
5205 sub process_respool {
5206 process_re_X('spool', @_);
5209 use Storable qw(thaw);
5213 my( $method, $job ) = ( shift, shift );
5214 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5216 my $param = thaw(decode_base64(shift));
5217 warn Dumper($param) if $DEBUG;
5228 my($method, $job, %param ) = @_;
5230 warn "re_X $method for job $job with param:\n".
5231 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5234 #some false laziness w/search/cust_bill.html
5236 my $orderby = 'ORDER BY cust_bill._date';
5238 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5240 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5242 my @cust_bill = qsearch( {
5243 #'select' => "cust_bill.*",
5244 'table' => 'cust_bill',
5245 'addl_from' => $addl_from,
5247 'extra_sql' => $extra_sql,
5248 'order_by' => $orderby,
5252 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5254 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5257 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5258 foreach my $cust_bill ( @cust_bill ) {
5259 $cust_bill->$method();
5261 if ( $job ) { #progressbar foo
5263 if ( time - $min_sec > $last ) {
5264 my $error = $job->update_statustext(
5265 int( 100 * $num / scalar(@cust_bill) )
5267 die $error if $error;
5278 =head1 CLASS METHODS
5284 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5289 my ($class, $start, $end) = @_;
5291 $class->paid_sql($start, $end). ' - '.
5292 $class->credited_sql($start, $end);
5297 Returns an SQL fragment to retreive the net amount (charged minus credited).
5302 my ($class, $start, $end) = @_;
5303 'charged - '. $class->credited_sql($start, $end);
5308 Returns an SQL fragment to retreive the amount paid against this invoice.
5313 my ($class, $start, $end) = @_;
5314 $start &&= "AND cust_bill_pay._date <= $start";
5315 $end &&= "AND cust_bill_pay._date > $end";
5316 $start = '' unless defined($start);
5317 $end = '' unless defined($end);
5318 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5319 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5324 Returns an SQL fragment to retreive the amount credited against this invoice.
5329 my ($class, $start, $end) = @_;
5330 $start &&= "AND cust_credit_bill._date <= $start";
5331 $end &&= "AND cust_credit_bill._date > $end";
5332 $start = '' unless defined($start);
5333 $end = '' unless defined($end);
5334 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5335 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5340 Returns an SQL fragment to retrieve the due date of an invoice.
5341 Currently only supported on PostgreSQL.
5346 my $conf = new FS::Conf;
5350 cust_bill.invoice_terms,
5351 cust_main.invoice_terms,
5352 \''.($conf->config('invoice_default_terms') || '').'\'
5353 ), E\'Net (\\\\d+)\'
5355 ) * 86400 + cust_bill._date'
5358 =item search_sql_where HASHREF
5360 Class method which returns an SQL WHERE fragment to search for parameters
5361 specified in HASHREF. Valid parameters are
5367 List reference of start date, end date, as UNIX timestamps.
5377 List reference of charged limits (exclusive).
5381 List reference of charged limits (exclusive).
5385 flag, return open invoices only
5389 flag, return net invoices only
5393 =item newest_percust
5397 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5401 sub search_sql_where {
5402 my($class, $param) = @_;
5404 warn "$me search_sql_where called with params: \n".
5405 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5411 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5412 push @search, "cust_main.agentnum = $1";
5416 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5417 push @search, "cust_bill.custnum = $1";
5421 if ( $param->{_date} ) {
5422 my($beginning, $ending) = @{$param->{_date}};
5424 push @search, "cust_bill._date >= $beginning",
5425 "cust_bill._date < $ending";
5429 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5430 push @search, "cust_bill.invnum >= $1";
5432 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5433 push @search, "cust_bill.invnum <= $1";
5437 if ( $param->{charged} ) {
5438 my @charged = ref($param->{charged})
5439 ? @{ $param->{charged} }
5440 : ($param->{charged});
5442 push @search, map { s/^charged/cust_bill.charged/; $_; }
5446 my $owed_sql = FS::cust_bill->owed_sql;
5449 if ( $param->{owed} ) {
5450 my @owed = ref($param->{owed})
5451 ? @{ $param->{owed} }
5453 push @search, map { s/^owed/$owed_sql/; $_; }
5458 push @search, "0 != $owed_sql"
5459 if $param->{'open'};
5460 push @search, '0 != '. FS::cust_bill->net_sql
5464 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5465 if $param->{'days'};
5468 if ( $param->{'newest_percust'} ) {
5470 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5471 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5473 my @newest_where = map { my $x = $_;
5474 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5477 grep ! /^cust_main./, @search;
5478 my $newest_where = scalar(@newest_where)
5479 ? ' AND '. join(' AND ', @newest_where)
5483 push @search, "cust_bill._date = (
5484 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5485 WHERE newest_cust_bill.custnum = cust_bill.custnum
5491 #agent virtualization
5492 my $curuser = $FS::CurrentUser::CurrentUser;
5493 if ( $curuser->username eq 'fs_queue'
5494 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5496 my $newuser = qsearchs('access_user', {
5497 'username' => $username,
5501 $curuser = $newuser;
5503 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5506 push @search, $curuser->agentnums_sql;
5508 join(' AND ', @search );
5520 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5521 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base