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 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3702 Generate section information for all items appearing on this invoice.
3703 This will only be called for multi-section invoices.
3705 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3706 related display records (L<FS::cust_bill_pkg_display>) and organize
3707 them into two groups ("early" and "late" according to whether they come
3708 before or after the total), then into sections. A subtotal is calculated
3711 Section descriptions are returned in sort weight order. Each consists
3712 of a hash containing:
3714 description: the package category name, escaped
3715 subtotal: the total charges in that section
3716 tax_section: a flag indicating that the section contains only tax charges
3717 summarized: same as tax_section, for some reason
3718 sort_weight: the package category's sort weight
3720 If 'condense' is set on the display record, it also contains everything
3721 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3722 coderefs to generate parts of the invoice. This is not advised.
3724 Takes way too many arguments, all mandatory:
3726 LATE: an arrayref to push the "late" section hashes onto. The "early"
3727 group is simply returned from the method. Yes, I know. Don't ask.
3729 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3730 Turning this on has the following effects:
3731 - Ignores display items with the 'summary' flag.
3732 - Combines all items into the "early" group.
3733 - Creates sections for all non-disabled package categories, even if they
3734 have no charges on this invoice, as well as a section with no name.
3736 ESCAPE: an escape function to use for section titles. Why not just
3737 let the calling environment escape things itself? Beats the heck out
3740 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3741 sorted list. If there are any of these, section subtotals exclude
3744 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3745 passed through to C<_condense_section()>.
3749 use vars qw(%pkg_category_cache);
3750 sub _items_sections {
3753 my $summarypage = shift;
3755 my $extra_sections = shift;
3759 my %late_subtotal = ();
3762 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3765 my $usage = $cust_bill_pkg->usage;
3767 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3768 next if ( $display->summary && $summarypage );
3770 my $section = $display->section;
3771 my $type = $display->type;
3773 $not_tax{$section} = 1
3774 unless $cust_bill_pkg->pkgnum == 0;
3776 if ( $display->post_total && !$summarypage ) {
3777 if (! $type || $type eq 'S') {
3778 $late_subtotal{$section} += $cust_bill_pkg->setup
3779 if $cust_bill_pkg->setup != 0;
3783 $late_subtotal{$section} += $cust_bill_pkg->recur
3784 if $cust_bill_pkg->recur != 0;
3787 if ($type && $type eq 'R') {
3788 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3789 if $cust_bill_pkg->recur != 0;
3792 if ($type && $type eq 'U') {
3793 $late_subtotal{$section} += $usage
3794 unless scalar(@$extra_sections);
3799 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3801 if (! $type || $type eq 'S') {
3802 $subtotal{$section} += $cust_bill_pkg->setup
3803 if $cust_bill_pkg->setup != 0;
3807 $subtotal{$section} += $cust_bill_pkg->recur
3808 if $cust_bill_pkg->recur != 0;
3811 if ($type && $type eq 'R') {
3812 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3813 if $cust_bill_pkg->recur != 0;
3816 if ($type && $type eq 'U') {
3817 $subtotal{$section} += $usage
3818 unless scalar(@$extra_sections);
3827 %pkg_category_cache = ();
3829 push @$late, map { { 'description' => &{$escape}($_),
3830 'subtotal' => $late_subtotal{$_},
3832 'sort_weight' => ( _pkg_category($_)
3833 ? _pkg_category($_)->weight
3836 ((_pkg_category($_) && _pkg_category($_)->condense)
3837 ? $self->_condense_section($format)
3841 sort _sectionsort keys %late_subtotal;
3844 if ( $summarypage ) {
3845 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3846 map { $_->categoryname } qsearch('pkg_category', {});
3847 push @sections, '' if exists($subtotal{''});
3849 @sections = keys %subtotal;
3852 my @early = map { { 'description' => &{$escape}($_),
3853 'subtotal' => $subtotal{$_},
3854 'summarized' => $not_tax{$_} ? '' : 'Y',
3855 'tax_section' => $not_tax{$_} ? '' : 'Y',
3856 'sort_weight' => ( _pkg_category($_)
3857 ? _pkg_category($_)->weight
3860 ((_pkg_category($_) && _pkg_category($_)->condense)
3861 ? $self->_condense_section($format)
3866 push @early, @$extra_sections if $extra_sections;
3868 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3872 #helper subs for above
3875 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3879 my $categoryname = shift;
3880 $pkg_category_cache{$categoryname} ||=
3881 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3884 my %condensed_format = (
3885 'label' => [ qw( Description Qty Amount ) ],
3887 sub { shift->{description} },
3888 sub { shift->{quantity} },
3889 sub { my($href, %opt) = @_;
3890 ($opt{dollar} || ''). $href->{amount};
3893 'align' => [ qw( l r r ) ],
3894 'span' => [ qw( 5 1 1 ) ], # unitprices?
3895 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3898 sub _condense_section {
3899 my ( $self, $format ) = ( shift, shift );
3901 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3902 qw( description_generator
3905 total_line_generator
3910 sub _condensed_generator_defaults {
3911 my ( $self, $format ) = ( shift, shift );
3912 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3921 sub _condensed_header_generator {
3922 my ( $self, $format ) = ( shift, shift );
3924 my ( $f, $prefix, $suffix, $separator, $column ) =
3925 _condensed_generator_defaults($format);
3927 if ($format eq 'latex') {
3928 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3929 $suffix = "\\\\\n\\hline";
3932 sub { my ($d,$a,$s,$w) = @_;
3933 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3935 } elsif ( $format eq 'html' ) {
3936 $prefix = '<th></th>';
3940 sub { my ($d,$a,$s,$w) = @_;
3941 return qq!<th align="$html_align{$a}">$d</th>!;
3949 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3951 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3954 $prefix. join($separator, @result). $suffix;
3959 sub _condensed_description_generator {
3960 my ( $self, $format ) = ( shift, shift );
3962 my ( $f, $prefix, $suffix, $separator, $column ) =
3963 _condensed_generator_defaults($format);
3965 my $money_char = '$';
3966 if ($format eq 'latex') {
3967 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3969 $separator = " & \n";
3971 sub { my ($d,$a,$s,$w) = @_;
3972 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3974 $money_char = '\\dollar';
3975 }elsif ( $format eq 'html' ) {
3976 $prefix = '"><td align="center"></td>';
3980 sub { my ($d,$a,$s,$w) = @_;
3981 return qq!<td align="$html_align{$a}">$d</td>!;
3983 #$money_char = $conf->config('money_char') || '$';
3984 $money_char = ''; # this is madness
3992 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3994 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3996 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3997 map { $f->{$_}->[$i] } qw(align span width)
4001 $prefix. join( $separator, @result ). $suffix;
4006 sub _condensed_total_generator {
4007 my ( $self, $format ) = ( shift, shift );
4009 my ( $f, $prefix, $suffix, $separator, $column ) =
4010 _condensed_generator_defaults($format);
4013 if ($format eq 'latex') {
4016 $separator = " & \n";
4018 sub { my ($d,$a,$s,$w) = @_;
4019 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4021 }elsif ( $format eq 'html' ) {
4025 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4027 sub { my ($d,$a,$s,$w) = @_;
4028 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4037 # my $r = &{$f->{fields}->[$i]}(@args);
4038 # $r .= ' Total' unless $i;
4040 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4042 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4043 map { $f->{$_}->[$i] } qw(align span width)
4047 $prefix. join( $separator, @result ). $suffix;
4052 =item total_line_generator FORMAT
4054 Returns a coderef used for generation of invoice total line items for this
4055 usage_class. FORMAT is either html or latex
4059 # should not be used: will have issues with hash element names (description vs
4060 # total_item and amount vs total_amount -- another array of functions?
4062 sub _condensed_total_line_generator {
4063 my ( $self, $format ) = ( shift, shift );
4065 my ( $f, $prefix, $suffix, $separator, $column ) =
4066 _condensed_generator_defaults($format);
4069 if ($format eq 'latex') {
4072 $separator = " & \n";
4074 sub { my ($d,$a,$s,$w) = @_;
4075 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4077 }elsif ( $format eq 'html' ) {
4081 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4083 sub { my ($d,$a,$s,$w) = @_;
4084 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4093 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4095 &{$column}( &{$f->{fields}->[$i]}(@args),
4096 map { $f->{$_}->[$i] } qw(align span width)
4100 $prefix. join( $separator, @result ). $suffix;
4105 #sub _items_extra_usage_sections {
4107 # my $escape = shift;
4109 # my %sections = ();
4111 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4112 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4114 # next unless $cust_bill_pkg->pkgnum > 0;
4116 # foreach my $section ( keys %usage_class ) {
4118 # my $usage = $cust_bill_pkg->usage($section);
4120 # next unless $usage && $usage > 0;
4122 # $sections{$section} ||= 0;
4123 # $sections{$section} += $usage;
4129 # map { { 'description' => &{$escape}($_),
4130 # 'subtotal' => $sections{$_},
4131 # 'summarized' => '',
4132 # 'tax_section' => '',
4135 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4139 sub _items_extra_usage_sections {
4141 my $conf = $self->conf;
4149 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4151 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4152 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4153 next unless $cust_bill_pkg->pkgnum > 0;
4155 foreach my $classnum ( keys %usage_class ) {
4156 my $section = $usage_class{$classnum}->classname;
4157 $classnums{$section} = $classnum;
4159 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4160 my $amount = $detail->amount;
4161 next unless $amount && $amount > 0;
4163 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4164 $sections{$section}{amount} += $amount; #subtotal
4165 $sections{$section}{calls}++;
4166 $sections{$section}{duration} += $detail->duration;
4168 my $desc = $detail->regionname;
4169 my $description = $desc;
4170 $description = substr($desc, 0, $maxlength). '...'
4171 if $format eq 'latex' && length($desc) > $maxlength;
4173 $lines{$section}{$desc} ||= {
4174 description => &{$escape}($description),
4175 #pkgpart => $part_pkg->pkgpart,
4176 pkgnum => $cust_bill_pkg->pkgnum,
4181 #unit_amount => $cust_bill_pkg->unitrecur,
4182 quantity => $cust_bill_pkg->quantity,
4183 product_code => 'N/A',
4184 ext_description => [],
4187 $lines{$section}{$desc}{amount} += $amount;
4188 $lines{$section}{$desc}{calls}++;
4189 $lines{$section}{$desc}{duration} += $detail->duration;
4195 my %sectionmap = ();
4196 foreach (keys %sections) {
4197 my $usage_class = $usage_class{$classnums{$_}};
4198 $sectionmap{$_} = { 'description' => &{$escape}($_),
4199 'amount' => $sections{$_}{amount}, #subtotal
4200 'calls' => $sections{$_}{calls},
4201 'duration' => $sections{$_}{duration},
4203 'tax_section' => '',
4204 'sort_weight' => $usage_class->weight,
4205 ( $usage_class->format
4206 ? ( map { $_ => $usage_class->$_($format) }
4207 qw( description_generator header_generator total_generator total_line_generator )
4214 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4218 foreach my $section ( keys %lines ) {
4219 foreach my $line ( keys %{$lines{$section}} ) {
4220 my $l = $lines{$section}{$line};
4221 $l->{section} = $sectionmap{$section};
4222 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4223 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4228 return(\@sections, \@lines);
4234 my $end = $self->_date;
4236 # start at date of previous invoice + 1 second or 0 if no previous invoice
4237 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4238 $start = 0 if !$start;
4241 my $cust_main = $self->cust_main;
4242 my @pkgs = $cust_main->all_pkgs;
4243 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4246 foreach my $pkg ( @pkgs ) {
4247 my @h_cust_svc = $pkg->h_cust_svc($end);
4248 foreach my $h_cust_svc ( @h_cust_svc ) {
4249 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4250 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4252 my $inserted = $h_cust_svc->date_inserted;
4253 my $deleted = $h_cust_svc->date_deleted;
4254 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4256 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4258 # DID either activated or ported in; cannot be both for same DID simultaneously
4259 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4260 && (!$phone_inserted->lnp_status
4261 || $phone_inserted->lnp_status eq ''
4262 || $phone_inserted->lnp_status eq 'native')) {
4265 else { # this one not so clean, should probably move to (h_)svc_phone
4266 my $phone_portedin = qsearchs( 'h_svc_phone',
4267 { 'svcnum' => $h_cust_svc->svcnum,
4268 'lnp_status' => 'portedin' },
4269 FS::h_svc_phone->sql_h_searchs($end),
4271 $num_portedin++ if $phone_portedin;
4274 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4275 if($deleted >= $start && $deleted <= $end && $phone_deleted
4276 && (!$phone_deleted->lnp_status
4277 || $phone_deleted->lnp_status ne 'portingout')) {
4280 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4281 && $phone_deleted->lnp_status
4282 && $phone_deleted->lnp_status eq 'portingout') {
4286 # increment usage minutes
4287 if ( $phone_inserted ) {
4288 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4289 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4292 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4295 # don't look at this service again
4296 push @seen, $h_cust_svc->svcnum;
4300 $minutes = sprintf("%d", $minutes);
4301 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4302 . "$num_deactivated Ported-Out: $num_portedout ",
4303 "Total Minutes: $minutes");
4306 sub _items_accountcode_cdr {
4311 my $section = { 'amount' => 0,
4314 'sort_weight' => '',
4316 'description' => 'Usage by Account Code',
4322 my %accountcodes = ();
4324 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4325 next unless $cust_bill_pkg->pkgnum > 0;
4327 my @header = $cust_bill_pkg->details_header;
4328 next unless scalar(@header);
4329 $section->{'header'} = join(',',@header);
4331 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4333 $section->{'header'} = $detail->formatted('format' => $format)
4334 if($detail->detail eq $section->{'header'});
4336 my $accountcode = $detail->accountcode;
4337 next unless $accountcode;
4339 my $amount = $detail->amount;
4340 next unless $amount && $amount > 0;
4342 $accountcodes{$accountcode} ||= {
4343 description => $accountcode,
4350 product_code => 'N/A',
4351 section => $section,
4352 ext_description => [ $section->{'header'} ],
4356 $section->{'amount'} += $amount;
4357 $accountcodes{$accountcode}{'amount'} += $amount;
4358 $accountcodes{$accountcode}{calls}++;
4359 $accountcodes{$accountcode}{duration} += $detail->duration;
4360 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4364 foreach my $l ( values %accountcodes ) {
4365 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4366 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4367 foreach my $sorted_detail ( @sorted_detail ) {
4368 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4370 delete $l->{detail_temp};
4374 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4376 return ($section,\@sorted_lines);
4379 sub _items_svc_phone_sections {
4381 my $conf = $self->conf;
4389 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4391 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4392 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4394 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4395 next unless $cust_bill_pkg->pkgnum > 0;
4397 my @header = $cust_bill_pkg->details_header;
4398 next unless scalar(@header);
4400 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4402 my $phonenum = $detail->phonenum;
4403 next unless $phonenum;
4405 my $amount = $detail->amount;
4406 next unless $amount && $amount > 0;
4408 $sections{$phonenum} ||= { 'amount' => 0,
4411 'sort_weight' => -1,
4412 'phonenum' => $phonenum,
4414 $sections{$phonenum}{amount} += $amount; #subtotal
4415 $sections{$phonenum}{calls}++;
4416 $sections{$phonenum}{duration} += $detail->duration;
4418 my $desc = $detail->regionname;
4419 my $description = $desc;
4420 $description = substr($desc, 0, $maxlength). '...'
4421 if $format eq 'latex' && length($desc) > $maxlength;
4423 $lines{$phonenum}{$desc} ||= {
4424 description => &{$escape}($description),
4425 #pkgpart => $part_pkg->pkgpart,
4433 product_code => 'N/A',
4434 ext_description => [],
4437 $lines{$phonenum}{$desc}{amount} += $amount;
4438 $lines{$phonenum}{$desc}{calls}++;
4439 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4441 my $line = $usage_class{$detail->classnum}->classname;
4442 $sections{"$phonenum $line"} ||=
4446 'sort_weight' => $usage_class{$detail->classnum}->weight,
4447 'phonenum' => $phonenum,
4448 'header' => [ @header ],
4450 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4451 $sections{"$phonenum $line"}{calls}++;
4452 $sections{"$phonenum $line"}{duration} += $detail->duration;
4454 $lines{"$phonenum $line"}{$desc} ||= {
4455 description => &{$escape}($description),
4456 #pkgpart => $part_pkg->pkgpart,
4464 product_code => 'N/A',
4465 ext_description => [],
4468 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4469 $lines{"$phonenum $line"}{$desc}{calls}++;
4470 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4471 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4472 $detail->formatted('format' => $format);
4477 my %sectionmap = ();
4478 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4479 foreach ( keys %sections ) {
4480 my @header = @{ $sections{$_}{header} || [] };
4482 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4483 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4484 my $usage_class = $summary ? $simple : $usage_simple;
4485 my $ending = $summary ? ' usage charges' : '';
4488 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4490 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4491 'amount' => $sections{$_}{amount}, #subtotal
4492 'calls' => $sections{$_}{calls},
4493 'duration' => $sections{$_}{duration},
4495 'tax_section' => '',
4496 'phonenum' => $sections{$_}{phonenum},
4497 'sort_weight' => $sections{$_}{sort_weight},
4498 'post_total' => $summary, #inspire pagebreak
4500 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4501 qw( description_generator
4504 total_line_generator
4511 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4512 $a->{sort_weight} <=> $b->{sort_weight}
4517 foreach my $section ( keys %lines ) {
4518 foreach my $line ( keys %{$lines{$section}} ) {
4519 my $l = $lines{$section}{$line};
4520 $l->{section} = $sectionmap{$section};
4521 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4522 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4527 if($conf->exists('phone_usage_class_summary')) {
4528 # this only works with Latex
4532 # after this, we'll have only two sections per DID:
4533 # Calls Summary and Calls Detail
4534 foreach my $section ( @sections ) {
4535 if($section->{'post_total'}) {
4536 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4537 $section->{'total_line_generator'} = sub { '' };
4538 $section->{'total_generator'} = sub { '' };
4539 $section->{'header_generator'} = sub { '' };
4540 $section->{'description_generator'} = '';
4541 push @newsections, $section;
4542 my %calls_detail = %$section;
4543 $calls_detail{'post_total'} = '';
4544 $calls_detail{'sort_weight'} = '';
4545 $calls_detail{'description_generator'} = sub { '' };
4546 $calls_detail{'header_generator'} = sub {
4547 return ' & Date/Time & Called Number & Duration & Price'
4548 if $format eq 'latex';
4551 $calls_detail{'description'} = 'Calls Detail: '
4552 . $section->{'phonenum'};
4553 push @newsections, \%calls_detail;
4557 # after this, each usage class is collapsed/summarized into a single
4558 # line under the Calls Summary section
4559 foreach my $newsection ( @newsections ) {
4560 if($newsection->{'post_total'}) { # this means Calls Summary
4561 foreach my $section ( @sections ) {
4562 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4563 && !$section->{'post_total'});
4564 my $newdesc = $section->{'description'};
4565 my $tn = $section->{'phonenum'};
4566 $newdesc =~ s/$tn//g;
4567 my $line = { ext_description => [],
4571 calls => $section->{'calls'},
4572 section => $newsection,
4573 duration => $section->{'duration'},
4574 description => $newdesc,
4575 amount => sprintf("%.2f",$section->{'amount'}),
4576 product_code => 'N/A',
4578 push @newlines, $line;
4583 # after this, Calls Details is populated with all CDRs
4584 foreach my $newsection ( @newsections ) {
4585 if(!$newsection->{'post_total'}) { # this means Calls Details
4586 foreach my $line ( @lines ) {
4587 next unless (scalar(@{$line->{'ext_description'}}) &&
4588 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4590 my @extdesc = @{$line->{'ext_description'}};
4592 foreach my $extdesc ( @extdesc ) {
4593 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4594 push @newextdesc, $extdesc;
4596 $line->{'ext_description'} = \@newextdesc;
4597 $line->{'section'} = $newsection;
4598 push @newlines, $line;
4603 return(\@newsections, \@newlines);
4606 return(\@sections, \@lines);
4610 sub _items { # seems to be unused
4613 #my @display = scalar(@_)
4615 # : qw( _items_previous _items_pkg );
4616 # #: qw( _items_pkg );
4617 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4618 my @display = qw( _items_previous _items_pkg );
4621 foreach my $display ( @display ) {
4622 push @b, $self->$display(@_);
4627 sub _items_previous {
4629 my $conf = $self->conf;
4630 my $cust_main = $self->cust_main;
4631 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4633 foreach ( @pr_cust_bill ) {
4634 my $date = $conf->exists('invoice_show_prior_due_date')
4635 ? 'due '. $_->due_date2str($date_format)
4636 : time2str($date_format, $_->_date);
4638 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4639 #'pkgpart' => 'N/A',
4641 'amount' => sprintf("%.2f", $_->owed),
4647 # 'description' => 'Previous Balance',
4648 # #'pkgpart' => 'N/A',
4649 # 'pkgnum' => 'N/A',
4650 # 'amount' => sprintf("%10.2f", $pr_total ),
4651 # 'ext_description' => [ map {
4652 # "Invoice ". $_->invnum.
4653 # " (". time2str("%x",$_->_date). ") ".
4654 # sprintf("%10.2f", $_->owed)
4655 # } @pr_cust_bill ],
4660 =item _items_pkg [ OPTIONS ]
4662 Return line item hashes for each package item on this invoice. Nearly
4665 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4667 The only OPTIONS accepted is 'section', which may point to a hashref
4668 with a key named 'condensed', which may have a true value. If it
4669 does, this method tries to merge identical items into items with
4670 'quantity' equal to the number of items (not the sum of their
4671 separate quantities, for some reason).
4679 warn "$me _items_pkg searching for all package line items\n"
4682 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4684 warn "$me _items_pkg filtering line items\n"
4686 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4688 if ($options{section} && $options{section}->{condensed}) {
4690 warn "$me _items_pkg condensing section\n"
4694 local $Storable::canonical = 1;
4695 foreach ( @items ) {
4697 delete $item->{ref};
4698 delete $item->{ext_description};
4699 my $key = freeze($item);
4700 $itemshash{$key} ||= 0;
4701 $itemshash{$key} ++; # += $item->{quantity};
4703 @items = sort { $a->{description} cmp $b->{description} }
4704 map { my $i = thaw($_);
4705 $i->{quantity} = $itemshash{$_};
4707 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4713 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4720 return 0 unless $a->itemdesc cmp $b->itemdesc;
4721 return -1 if $b->itemdesc eq 'Tax';
4722 return 1 if $a->itemdesc eq 'Tax';
4723 return -1 if $b->itemdesc eq 'Other surcharges';
4724 return 1 if $a->itemdesc eq 'Other surcharges';
4725 $a->itemdesc cmp $b->itemdesc;
4730 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4731 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4734 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4736 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4737 list of hashrefs describing the line items they generate on the invoice.
4739 OPTIONS may include:
4741 format: the invoice format.
4743 escape_function: the function used to escape strings.
4745 format_function: the function used to format CDRs.
4747 section: a hashref containing 'description'; if this is present,
4748 cust_bill_pkg_display records not belonging to this section are
4751 multisection: a flag indicating that this is a multisection invoice,
4752 which does something complicated.
4754 multilocation: a flag to display the location label for the package.
4756 Returns a list of hashrefs, each of which may contain:
4758 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4759 ext_description, which is an arrayref of detail lines to show below
4764 sub _items_cust_bill_pkg {
4766 my $conf = $self->conf;
4767 my $cust_bill_pkgs = shift;
4770 my $format = $opt{format} || '';
4771 my $escape_function = $opt{escape_function} || sub { shift };
4772 my $format_function = $opt{format_function} || '';
4773 my $unsquelched = $opt{unsquelched} || ''; #unused
4774 my $section = $opt{section}->{description} if $opt{section};
4775 my $summary_page = $opt{summary_page} || ''; #unused
4776 my $multilocation = $opt{multilocation} || '';
4777 my $multisection = $opt{multisection} || '';
4778 my $discount_show_always = 0;
4780 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4783 my ($s, $r, $u) = ( undef, undef, undef );
4784 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4787 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4788 if ( $_ && !$cust_bill_pkg->hidden ) {
4789 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4790 $_->{amount} =~ s/^\-0\.00$/0.00/;
4791 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4793 if $_->{amount} != 0
4794 || $discount_show_always
4795 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4796 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4802 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4803 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4806 foreach my $display ( grep { defined($section)
4807 ? $_->section eq $section
4810 #grep { !$_->summary || !$summary_page } # bunk!
4811 grep { !$_->summary || $multisection }
4812 $cust_bill_pkg->cust_bill_pkg_display
4816 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4817 $display->billpkgdisplaynum. "\n"
4820 my $type = $display->type;
4822 my $desc = $cust_bill_pkg->desc;
4823 $desc = substr($desc, 0, $maxlength). '...'
4824 if $format eq 'latex' && length($desc) > $maxlength;
4826 my %details_opt = ( 'format' => $format,
4827 'escape_function' => $escape_function,
4828 'format_function' => $format_function,
4831 if ( $cust_bill_pkg->pkgnum > 0 ) {
4833 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4836 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4838 # start/end dates for invoice formats that do nonstandard
4840 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4842 if ( (!$type || $type eq 'S')
4843 && ( $cust_bill_pkg->setup != 0
4844 || $cust_bill_pkg->setup_show_zero
4849 warn "$me _items_cust_bill_pkg adding setup\n"
4852 my $description = $desc;
4853 $description .= ' Setup'
4854 if $cust_bill_pkg->recur != 0
4855 || $discount_show_always
4856 || $cust_bill_pkg->recur_show_zero;
4859 unless ( $cust_pkg->part_pkg->hide_svc_detail
4860 || $cust_bill_pkg->hidden )
4863 push @d, map &{$escape_function}($_),
4864 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4865 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4867 if ( $multilocation ) {
4868 my $loc = $cust_pkg->location_label;
4869 $loc = substr($loc, 0, $maxlength). '...'
4870 if $format eq 'latex' && length($loc) > $maxlength;
4871 push @d, &{$escape_function}($loc);
4876 push @d, $cust_bill_pkg->details(%details_opt)
4877 if $cust_bill_pkg->recur == 0;
4879 if ( $cust_bill_pkg->hidden ) {
4880 $s->{amount} += $cust_bill_pkg->setup;
4881 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4882 push @{ $s->{ext_description} }, @d;
4886 description => $description,
4887 #pkgpart => $part_pkg->pkgpart,
4888 pkgnum => $cust_bill_pkg->pkgnum,
4889 amount => $cust_bill_pkg->setup,
4890 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4891 unit_amount => $cust_bill_pkg->unitsetup,
4892 quantity => $cust_bill_pkg->quantity,
4893 ext_description => \@d,
4899 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4901 $cust_bill_pkg->recur != 0
4902 || $cust_bill_pkg->setup == 0
4903 || $discount_show_always
4904 || $cust_bill_pkg->recur_show_zero
4909 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4912 my $is_summary = $display->summary;
4913 my $description = ($is_summary && $type && $type eq 'U')
4914 ? "Usage charges" : $desc;
4916 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4917 " - ". time2str($date_format, $cust_bill_pkg->edate).
4919 unless $conf->exists('disable_line_item_date_ranges')
4920 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4924 #at least until cust_bill_pkg has "past" ranges in addition to
4925 #the "future" sdate/edate ones... see #3032
4926 my @dates = ( $self->_date );
4927 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4928 push @dates, $prev->sdate if $prev;
4929 push @dates, undef if !$prev;
4931 unless ( $cust_pkg->part_pkg->hide_svc_detail
4932 || $cust_bill_pkg->itemdesc
4933 || $cust_bill_pkg->hidden
4934 || $is_summary && $type && $type eq 'U' )
4937 warn "$me _items_cust_bill_pkg adding service details\n"
4940 push @d, map &{$escape_function}($_),
4941 $cust_pkg->h_labels_short(@dates, 'I')
4942 #$cust_bill_pkg->edate,
4943 #$cust_bill_pkg->sdate)
4944 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4946 warn "$me _items_cust_bill_pkg done adding service details\n"
4949 if ( $multilocation ) {
4950 my $loc = $cust_pkg->location_label;
4951 $loc = substr($loc, 0, $maxlength). '...'
4952 if $format eq 'latex' && length($loc) > $maxlength;
4953 push @d, &{$escape_function}($loc);
4958 unless ( $is_summary ) {
4959 warn "$me _items_cust_bill_pkg adding details\n"
4962 #instead of omitting details entirely in this case (unwanted side
4963 # effects), just omit CDRs
4964 $details_opt{'format_function'} = sub { () }
4965 if $type && $type eq 'R';
4967 push @d, $cust_bill_pkg->details(%details_opt);
4970 warn "$me _items_cust_bill_pkg calculating amount\n"
4975 $amount = $cust_bill_pkg->recur;
4976 } elsif ($type eq 'R') {
4977 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4978 } elsif ($type eq 'U') {
4979 $amount = $cust_bill_pkg->usage;
4982 if ( !$type || $type eq 'R' ) {
4984 warn "$me _items_cust_bill_pkg adding recur\n"
4987 if ( $cust_bill_pkg->hidden ) {
4988 $r->{amount} += $amount;
4989 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4990 push @{ $r->{ext_description} }, @d;
4993 description => $description,
4994 #pkgpart => $part_pkg->pkgpart,
4995 pkgnum => $cust_bill_pkg->pkgnum,
4997 recur_show_zero => $cust_bill_pkg->recur_show_zero,
4998 unit_amount => $cust_bill_pkg->unitrecur,
4999 quantity => $cust_bill_pkg->quantity,
5001 ext_description => \@d,
5005 } else { # $type eq 'U'
5007 warn "$me _items_cust_bill_pkg adding usage\n"
5010 if ( $cust_bill_pkg->hidden ) {
5011 $u->{amount} += $amount;
5012 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5013 push @{ $u->{ext_description} }, @d;
5016 description => $description,
5017 #pkgpart => $part_pkg->pkgpart,
5018 pkgnum => $cust_bill_pkg->pkgnum,
5020 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5021 unit_amount => $cust_bill_pkg->unitrecur,
5022 quantity => $cust_bill_pkg->quantity,
5024 ext_description => \@d,
5029 } # recurring or usage with recurring charge
5031 } else { #pkgnum tax or one-shot line item (??)
5033 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5036 if ( $cust_bill_pkg->setup != 0 ) {
5038 'description' => $desc,
5039 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5042 if ( $cust_bill_pkg->recur != 0 ) {
5044 'description' => "$desc (".
5045 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5046 time2str($date_format, $cust_bill_pkg->edate). ')',
5047 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5055 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5056 && $conf->exists('discount-show-always'));
5060 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5062 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5063 $_->{amount} =~ s/^\-0\.00$/0.00/;
5064 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5066 if $_->{amount} != 0
5067 || $discount_show_always
5068 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5069 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5073 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5080 sub _items_credits {
5081 my( $self, %opt ) = @_;
5082 my $trim_len = $opt{'trim_len'} || 60;
5086 foreach ( $self->cust_credited ) {
5088 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5090 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5091 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5092 $reason = " ($reason) " if $reason;
5095 #'description' => 'Credit ref\#'. $_->crednum.
5096 # " (". time2str("%x",$_->cust_credit->_date) .")".
5098 'description' => $self->mt('Credit applied').' '.
5099 time2str($date_format,$_->cust_credit->_date). $reason,
5100 'amount' => sprintf("%.2f",$_->amount),
5108 sub _items_payments {
5112 #get & print payments
5113 foreach ( $self->cust_bill_pay ) {
5115 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5118 'description' => $self->mt('Payment received').' '.
5119 time2str($date_format,$_->cust_pay->_date ),
5120 'amount' => sprintf("%.2f", $_->amount )
5128 =item call_details [ OPTION => VALUE ... ]
5130 Returns an array of CSV strings representing the call details for this invoice
5131 The only option available is the boolean prepend_billed_number
5136 my ($self, %opt) = @_;
5138 my $format_function = sub { shift };
5140 if ($opt{prepend_billed_number}) {
5141 $format_function = sub {
5145 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5150 my @details = map { $_->details( 'format_function' => $format_function,
5151 'escape_function' => sub{ return() },
5155 $self->cust_bill_pkg;
5156 my $header = $details[0];
5157 ( $header, grep { $_ ne $header } @details );
5167 =item process_reprint
5171 sub process_reprint {
5172 process_re_X('print', @_);
5175 =item process_reemail
5179 sub process_reemail {
5180 process_re_X('email', @_);
5188 process_re_X('fax', @_);
5196 process_re_X('ftp', @_);
5203 sub process_respool {
5204 process_re_X('spool', @_);
5207 use Storable qw(thaw);
5211 my( $method, $job ) = ( shift, shift );
5212 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5214 my $param = thaw(decode_base64(shift));
5215 warn Dumper($param) if $DEBUG;
5226 my($method, $job, %param ) = @_;
5228 warn "re_X $method for job $job with param:\n".
5229 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5232 #some false laziness w/search/cust_bill.html
5234 my $orderby = 'ORDER BY cust_bill._date';
5236 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5238 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5240 my @cust_bill = qsearch( {
5241 #'select' => "cust_bill.*",
5242 'table' => 'cust_bill',
5243 'addl_from' => $addl_from,
5245 'extra_sql' => $extra_sql,
5246 'order_by' => $orderby,
5250 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5252 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5255 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5256 foreach my $cust_bill ( @cust_bill ) {
5257 $cust_bill->$method();
5259 if ( $job ) { #progressbar foo
5261 if ( time - $min_sec > $last ) {
5262 my $error = $job->update_statustext(
5263 int( 100 * $num / scalar(@cust_bill) )
5265 die $error if $error;
5276 =head1 CLASS METHODS
5282 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5287 my ($class, $start, $end) = @_;
5289 $class->paid_sql($start, $end). ' - '.
5290 $class->credited_sql($start, $end);
5295 Returns an SQL fragment to retreive the net amount (charged minus credited).
5300 my ($class, $start, $end) = @_;
5301 'charged - '. $class->credited_sql($start, $end);
5306 Returns an SQL fragment to retreive the amount paid against this invoice.
5311 my ($class, $start, $end) = @_;
5312 $start &&= "AND cust_bill_pay._date <= $start";
5313 $end &&= "AND cust_bill_pay._date > $end";
5314 $start = '' unless defined($start);
5315 $end = '' unless defined($end);
5316 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5317 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5322 Returns an SQL fragment to retreive the amount credited against this invoice.
5327 my ($class, $start, $end) = @_;
5328 $start &&= "AND cust_credit_bill._date <= $start";
5329 $end &&= "AND cust_credit_bill._date > $end";
5330 $start = '' unless defined($start);
5331 $end = '' unless defined($end);
5332 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5333 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5338 Returns an SQL fragment to retrieve the due date of an invoice.
5339 Currently only supported on PostgreSQL.
5344 my $conf = new FS::Conf;
5348 cust_bill.invoice_terms,
5349 cust_main.invoice_terms,
5350 \''.($conf->config('invoice_default_terms') || '').'\'
5351 ), E\'Net (\\\\d+)\'
5353 ) * 86400 + cust_bill._date'
5356 =item search_sql_where HASHREF
5358 Class method which returns an SQL WHERE fragment to search for parameters
5359 specified in HASHREF. Valid parameters are
5365 List reference of start date, end date, as UNIX timestamps.
5375 List reference of charged limits (exclusive).
5379 List reference of charged limits (exclusive).
5383 flag, return open invoices only
5387 flag, return net invoices only
5391 =item newest_percust
5395 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5399 sub search_sql_where {
5400 my($class, $param) = @_;
5402 warn "$me search_sql_where called with params: \n".
5403 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5409 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5410 push @search, "cust_main.agentnum = $1";
5414 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5415 push @search, "cust_bill.custnum = $1";
5419 if ( $param->{_date} ) {
5420 my($beginning, $ending) = @{$param->{_date}};
5422 push @search, "cust_bill._date >= $beginning",
5423 "cust_bill._date < $ending";
5427 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5428 push @search, "cust_bill.invnum >= $1";
5430 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5431 push @search, "cust_bill.invnum <= $1";
5435 if ( $param->{charged} ) {
5436 my @charged = ref($param->{charged})
5437 ? @{ $param->{charged} }
5438 : ($param->{charged});
5440 push @search, map { s/^charged/cust_bill.charged/; $_; }
5444 my $owed_sql = FS::cust_bill->owed_sql;
5447 if ( $param->{owed} ) {
5448 my @owed = ref($param->{owed})
5449 ? @{ $param->{owed} }
5451 push @search, map { s/^owed/$owed_sql/; $_; }
5456 push @search, "0 != $owed_sql"
5457 if $param->{'open'};
5458 push @search, '0 != '. FS::cust_bill->net_sql
5462 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5463 if $param->{'days'};
5466 if ( $param->{'newest_percust'} ) {
5468 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5469 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5471 my @newest_where = map { my $x = $_;
5472 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5475 grep ! /^cust_main./, @search;
5476 my $newest_where = scalar(@newest_where)
5477 ? ' AND '. join(' AND ', @newest_where)
5481 push @search, "cust_bill._date = (
5482 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5483 WHERE newest_cust_bill.custnum = cust_bill.custnum
5489 #agent virtualization
5490 my $curuser = $FS::CurrentUser::CurrentUser;
5491 if ( $curuser->username eq 'fs_queue'
5492 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5494 my $newuser = qsearchs('access_user', {
5495 'username' => $username,
5499 $curuser = $newuser;
5501 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5504 push @search, $curuser->agentnums_sql;
5506 join(' AND ', @search );
5518 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5519 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base