4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
155 Creates a new invoice. To add the invoice to the database, see L<"insert">.
156 Invoices are normally created by calling the bill method of a customer object
157 (see L<FS::cust_main>).
161 sub table { 'cust_bill'; }
163 sub cust_linked { $_[0]->cust_main_custnum; }
164 sub cust_unlinked_msg {
166 "WARNING: can't find cust_main.custnum ". $self->custnum.
167 ' (cust_bill.invnum '. $self->invnum. ')';
172 Adds this invoice to the database ("Posts" the invoice). If there is an error,
173 returns the error, otherwise returns false.
179 warn "$me insert called\n" if $DEBUG;
181 local $SIG{HUP} = 'IGNORE';
182 local $SIG{INT} = 'IGNORE';
183 local $SIG{QUIT} = 'IGNORE';
184 local $SIG{TERM} = 'IGNORE';
185 local $SIG{TSTP} = 'IGNORE';
186 local $SIG{PIPE} = 'IGNORE';
188 my $oldAutoCommit = $FS::UID::AutoCommit;
189 local $FS::UID::AutoCommit = 0;
192 my $error = $self->SUPER::insert;
194 $dbh->rollback if $oldAutoCommit;
198 if ( $self->get('cust_bill_pkg') ) {
199 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
200 $cust_bill_pkg->invnum($self->invnum);
201 my $error = $cust_bill_pkg->insert;
203 $dbh->rollback if $oldAutoCommit;
204 return "can't create invoice line item: $error";
209 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
216 This method now works but you probably shouldn't use it. Instead, apply a
217 credit against the invoice.
219 Using this method to delete invoices outright is really, really bad. There
220 would be no record you ever posted this invoice, and there are no check to
221 make sure charged = 0 or that there are no associated cust_bill_pkg records.
223 Really, don't use it.
229 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
231 local $SIG{HUP} = 'IGNORE';
232 local $SIG{INT} = 'IGNORE';
233 local $SIG{QUIT} = 'IGNORE';
234 local $SIG{TERM} = 'IGNORE';
235 local $SIG{TSTP} = 'IGNORE';
236 local $SIG{PIPE} = 'IGNORE';
238 my $oldAutoCommit = $FS::UID::AutoCommit;
239 local $FS::UID::AutoCommit = 0;
242 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 all discount plans (L<FS::discount_plan>) for this invoice, as a
755 hash keyed by term length.
761 FS::discount_plan->all($self);
766 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
773 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
775 foreach (@taxlines) { $total += $_->setup; }
781 Returns the amount owed (still outstanding) on this invoice, which is charged
782 minus all payment applications (see L<FS::cust_bill_pay>) and credit
783 applications (see L<FS::cust_credit_bill>).
789 my $balance = $self->charged;
790 $balance -= $_->amount foreach ( $self->cust_bill_pay );
791 $balance -= $_->amount foreach ( $self->cust_credited );
792 $balance = sprintf( "%.2f", $balance);
793 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
798 my( $self, $pkgnum ) = @_;
800 #my $balance = $self->charged;
802 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
804 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
805 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
807 $balance = sprintf( "%.2f", $balance);
808 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
814 Returns true if this invoice should be hidden. See the
815 selfservice-hide_invoices-taxclass configuraiton setting.
821 my $conf = $self->conf;
822 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
824 my @cust_bill_pkg = $self->cust_bill_pkg;
825 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
826 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
829 =item apply_payments_and_credits [ OPTION => VALUE ... ]
831 Applies unapplied payments and credits to this invoice.
833 A hash of optional arguments may be passed. Currently "manual" is supported.
834 If true, a payment receipt is sent instead of a statement when
835 'payment_receipt_email' configuration option is set.
837 If there is an error, returns the error, otherwise returns false.
841 sub apply_payments_and_credits {
842 my( $self, %options ) = @_;
843 my $conf = $self->conf;
845 local $SIG{HUP} = 'IGNORE';
846 local $SIG{INT} = 'IGNORE';
847 local $SIG{QUIT} = 'IGNORE';
848 local $SIG{TERM} = 'IGNORE';
849 local $SIG{TSTP} = 'IGNORE';
850 local $SIG{PIPE} = 'IGNORE';
852 my $oldAutoCommit = $FS::UID::AutoCommit;
853 local $FS::UID::AutoCommit = 0;
856 $self->select_for_update; #mutex
858 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
859 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
861 if ( $conf->exists('pkg-balances') ) {
862 # limit @payments & @credits to those w/ a pkgnum grepped from $self
863 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
864 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
865 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
868 while ( $self->owed > 0 and ( @payments || @credits ) ) {
871 if ( @payments && @credits ) {
873 #decide which goes first by weight of top (unapplied) line item
875 my @open_lineitems = $self->open_cust_bill_pkg;
878 max( map { $_->part_pkg->pay_weight || 0 }
883 my $max_credit_weight =
884 max( map { $_->part_pkg->credit_weight || 0 }
890 #if both are the same... payments first? it has to be something
891 if ( $max_pay_weight >= $max_credit_weight ) {
897 } elsif ( @payments ) {
899 } elsif ( @credits ) {
902 die "guru meditation #12 and 35";
906 if ( $app eq 'pay' ) {
908 my $payment = shift @payments;
909 $unapp_amount = $payment->unapplied;
910 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
911 $app->pkgnum( $payment->pkgnum )
912 if $conf->exists('pkg-balances') && $payment->pkgnum;
914 } elsif ( $app eq 'credit' ) {
916 my $credit = shift @credits;
917 $unapp_amount = $credit->credited;
918 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
919 $app->pkgnum( $credit->pkgnum )
920 if $conf->exists('pkg-balances') && $credit->pkgnum;
923 die "guru meditation #12 and 35";
927 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
928 warn "owed_pkgnum ". $app->pkgnum;
929 $owed = $self->owed_pkgnum($app->pkgnum);
933 next unless $owed > 0;
935 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
936 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
938 $app->invnum( $self->invnum );
940 my $error = $app->insert(%options);
942 $dbh->rollback if $oldAutoCommit;
943 return "Error inserting ". $app->table. " record: $error";
945 die $error if $error;
949 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
954 =item generate_email OPTION => VALUE ...
962 sender address, required
966 alternate template name, optional
970 text attachment arrayref, optional
974 email subject, optional
978 notice name instead of "Invoice", optional
982 Returns an argument list to be passed to L<FS::Misc::send_email>.
992 my $conf = $self->conf;
994 my $me = '[FS::cust_bill::generate_email]';
997 'from' => $args{'from'},
998 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1002 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1003 'template' => $args{'template'},
1004 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1005 'no_coupon' => $args{'no_coupon'},
1008 my $cust_main = $self->cust_main;
1010 if (ref($args{'to'}) eq 'ARRAY') {
1011 $return{'to'} = $args{'to'};
1013 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1014 $cust_main->invoicing_list
1018 if ( $conf->exists('invoice_html') ) {
1020 warn "$me creating HTML/text multipart message"
1023 $return{'nobody'} = 1;
1025 my $alternative = build MIME::Entity
1026 'Type' => 'multipart/alternative',
1027 #'Encoding' => '7bit',
1028 'Disposition' => 'inline'
1032 if ( $conf->exists('invoice_email_pdf')
1033 and scalar($conf->config('invoice_email_pdf_note')) ) {
1035 warn "$me using 'invoice_email_pdf_note' in multipart message"
1037 $data = [ map { $_ . "\n" }
1038 $conf->config('invoice_email_pdf_note')
1043 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1045 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1046 $data = $args{'print_text'};
1048 $data = [ $self->print_text(\%opt) ];
1053 $alternative->attach(
1054 'Type' => 'text/plain',
1055 'Encoding' => 'quoted-printable',
1056 #'Encoding' => '7bit',
1058 'Disposition' => 'inline',
1065 if ( $conf->exists('invoice_email_pdf')
1066 and scalar($conf->config('invoice_email_pdf_note')) ) {
1068 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1072 $args{'from'} =~ /\@([\w\.\-]+)/;
1073 my $from = $1 || 'example.com';
1074 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1077 my $agentnum = $cust_main->agentnum;
1078 if ( defined($args{'template'}) && length($args{'template'})
1079 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1082 $logo = 'logo_'. $args{'template'}. '.png';
1086 my $image_data = $conf->config_binary( $logo, $agentnum);
1088 $image = build MIME::Entity
1089 'Type' => 'image/png',
1090 'Encoding' => 'base64',
1091 'Data' => $image_data,
1092 'Filename' => 'logo.png',
1093 'Content-ID' => "<$content_id>",
1096 if ($conf->exists('invoice-barcode')) {
1097 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1098 $barcode = build MIME::Entity
1099 'Type' => 'image/png',
1100 'Encoding' => 'base64',
1101 'Data' => $self->invoice_barcode(0),
1102 'Filename' => 'barcode.png',
1103 'Content-ID' => "<$barcode_content_id>",
1105 $opt{'barcode_cid'} = $barcode_content_id;
1108 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1111 $alternative->attach(
1112 'Type' => 'text/html',
1113 'Encoding' => 'quoted-printable',
1114 'Data' => [ '<html>',
1117 ' '. encode_entities($return{'subject'}),
1120 ' <body bgcolor="#e8e8e8">',
1125 'Disposition' => 'inline',
1126 #'Filename' => 'invoice.pdf',
1130 my @otherparts = ();
1131 if ( $cust_main->email_csv_cdr ) {
1133 push @otherparts, build MIME::Entity
1134 'Type' => 'text/csv',
1135 'Encoding' => '7bit',
1136 'Data' => [ map { "$_\n" }
1137 $self->call_details('prepend_billed_number' => 1)
1139 'Disposition' => 'attachment',
1140 'Filename' => 'usage-'. $self->invnum. '.csv',
1145 if ( $conf->exists('invoice_email_pdf') ) {
1150 # multipart/alternative
1156 my $related = build MIME::Entity 'Type' => 'multipart/related',
1157 'Encoding' => '7bit';
1159 #false laziness w/Misc::send_email
1160 $related->head->replace('Content-type',
1161 $related->mime_type.
1162 '; boundary="'. $related->head->multipart_boundary. '"'.
1163 '; type=multipart/alternative'
1166 $related->add_part($alternative);
1168 $related->add_part($image) if $image;
1170 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1172 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1176 #no other attachment:
1178 # multipart/alternative
1183 $return{'content-type'} = 'multipart/related';
1184 if ($conf->exists('invoice-barcode') && $barcode) {
1185 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1187 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1189 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1190 #$return{'disposition'} = 'inline';
1196 if ( $conf->exists('invoice_email_pdf') ) {
1197 warn "$me creating PDF attachment"
1200 #mime parts arguments a la MIME::Entity->build().
1201 $return{'mimeparts'} = [
1202 { $self->mimebuild_pdf(\%opt) }
1206 if ( $conf->exists('invoice_email_pdf')
1207 and scalar($conf->config('invoice_email_pdf_note')) ) {
1209 warn "$me using 'invoice_email_pdf_note'"
1211 $return{'body'} = [ map { $_ . "\n" }
1212 $conf->config('invoice_email_pdf_note')
1217 warn "$me not using 'invoice_email_pdf_note'"
1219 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1220 $return{'body'} = $args{'print_text'};
1222 $return{'body'} = [ $self->print_text(\%opt) ];
1235 Returns a list suitable for passing to MIME::Entity->build(), representing
1236 this invoice as PDF attachment.
1243 'Type' => 'application/pdf',
1244 'Encoding' => 'base64',
1245 'Data' => [ $self->print_pdf(@_) ],
1246 'Disposition' => 'attachment',
1247 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1251 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1253 Sends this invoice to the destinations configured for this customer: sends
1254 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1256 Options can be passed as a hashref (recommended) or as a list of up to
1257 four values for templatename, agentnum, invoice_from and amount.
1259 I<template>, if specified, is the name of a suffix for alternate invoices.
1261 I<agentnum>, if specified, means that this invoice will only be sent for customers
1262 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1263 single agent) or an arrayref of agentnums.
1265 I<invoice_from>, if specified, overrides the default email invoice From: address.
1267 I<amount>, if specified, only sends the invoice if the total amount owed on this
1268 invoice and all older invoices is greater than the specified amount.
1270 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1274 sub queueable_send {
1277 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1278 or die "invalid invoice number: " . $opt{invnum};
1280 my @args = ( $opt{template}, $opt{agentnum} );
1281 push @args, $opt{invoice_from}
1282 if exists($opt{invoice_from}) && $opt{invoice_from};
1284 my $error = $self->send( @args );
1285 die $error if $error;
1291 my $conf = $self->conf;
1293 my( $template, $invoice_from, $notice_name );
1295 my $balance_over = 0;
1299 $template = $opt->{'template'} || '';
1300 if ( $agentnums = $opt->{'agentnum'} ) {
1301 $agentnums = [ $agentnums ] unless ref($agentnums);
1303 $invoice_from = $opt->{'invoice_from'};
1304 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1305 $notice_name = $opt->{'notice_name'};
1307 $template = scalar(@_) ? shift : '';
1308 if ( scalar(@_) && $_[0] ) {
1309 $agentnums = ref($_[0]) ? shift : [ shift ];
1311 $invoice_from = shift if scalar(@_);
1312 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1315 return 'N/A' unless ! $agentnums
1316 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1319 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1321 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1322 $conf->config('invoice_from', $self->cust_main->agentnum );
1325 'template' => $template,
1326 'invoice_from' => $invoice_from,
1327 'notice_name' => ( $notice_name || 'Invoice' ),
1330 my @invoicing_list = $self->cust_main->invoicing_list;
1332 #$self->email_invoice(\%opt)
1334 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1336 #$self->print_invoice(\%opt)
1338 if grep { $_ eq 'POST' } @invoicing_list; #postal
1340 $self->fax_invoice(\%opt)
1341 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1347 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1349 Emails this invoice.
1351 Options can be passed as a hashref (recommended) or as a list of up to
1352 two values for templatename and invoice_from.
1354 I<template>, if specified, is the name of a suffix for alternate invoices.
1356 I<invoice_from>, if specified, overrides the default email invoice From: address.
1358 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1362 sub queueable_email {
1365 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1366 or die "invalid invoice number: " . $opt{invnum};
1368 my %args = ( 'template' => $opt{template} );
1369 $args{$_} = $opt{$_}
1370 foreach grep { exists($opt{$_}) && $opt{$_} }
1371 qw( invoice_from notice_name no_coupon );
1373 my $error = $self->email( \%args );
1374 die $error if $error;
1378 #sub email_invoice {
1381 return if $self->hide;
1382 my $conf = $self->conf;
1384 my( $template, $invoice_from, $notice_name, $no_coupon );
1387 $template = $opt->{'template'} || '';
1388 $invoice_from = $opt->{'invoice_from'};
1389 $notice_name = $opt->{'notice_name'} || 'Invoice';
1390 $no_coupon = $opt->{'no_coupon'} || 0;
1392 $template = scalar(@_) ? shift : '';
1393 $invoice_from = shift if scalar(@_);
1394 $notice_name = 'Invoice';
1398 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1399 $conf->config('invoice_from', $self->cust_main->agentnum );
1401 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1402 $self->cust_main->invoicing_list;
1404 if ( ! @invoicing_list ) { #no recipients
1405 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1406 die 'No recipients for customer #'. $self->custnum;
1408 #default: better to notify this person than silence
1409 @invoicing_list = ($invoice_from);
1413 my $subject = $self->email_subject($template);
1415 my $error = send_email(
1416 $self->generate_email(
1417 'from' => $invoice_from,
1418 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1419 'subject' => $subject,
1420 'template' => $template,
1421 'notice_name' => $notice_name,
1422 'no_coupon' => $no_coupon,
1425 die "can't email invoice: $error\n" if $error;
1426 #die "$error\n" if $error;
1432 my $conf = $self->conf;
1434 #my $template = scalar(@_) ? shift : '';
1437 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1440 my $cust_main = $self->cust_main;
1441 my $name = $cust_main->name;
1442 my $name_short = $cust_main->name_short;
1443 my $invoice_number = $self->invnum;
1444 my $invoice_date = $self->_date_pretty;
1446 eval qq("$subject");
1449 =item lpr_data HASHREF | [ TEMPLATE ]
1451 Returns the postscript or plaintext for this invoice as an arrayref.
1453 Options can be passed as a hashref (recommended) or as a single optional value
1456 I<template>, if specified, is the name of a suffix for alternate invoices.
1458 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1464 my $conf = $self->conf;
1465 my( $template, $notice_name );
1468 $template = $opt->{'template'} || '';
1469 $notice_name = $opt->{'notice_name'} || 'Invoice';
1471 $template = scalar(@_) ? shift : '';
1472 $notice_name = 'Invoice';
1476 'template' => $template,
1477 'notice_name' => $notice_name,
1480 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1481 [ $self->$method( \%opt ) ];
1484 =item print HASHREF | [ TEMPLATE ]
1486 Prints this invoice.
1488 Options can be passed as a hashref (recommended) or as a single optional
1491 I<template>, if specified, is the name of a suffix for alternate invoices.
1493 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1497 #sub print_invoice {
1500 return if $self->hide;
1501 my $conf = $self->conf;
1503 my( $template, $notice_name );
1506 $template = $opt->{'template'} || '';
1507 $notice_name = $opt->{'notice_name'} || 'Invoice';
1509 $template = scalar(@_) ? shift : '';
1510 $notice_name = 'Invoice';
1514 'template' => $template,
1515 'notice_name' => $notice_name,
1518 if($conf->exists('invoice_print_pdf')) {
1519 # Add the invoice to the current batch.
1520 $self->batch_invoice(\%opt);
1523 do_print $self->lpr_data(\%opt);
1527 =item fax_invoice HASHREF | [ TEMPLATE ]
1531 Options can be passed as a hashref (recommended) or as a single optional
1534 I<template>, if specified, is the name of a suffix for alternate invoices.
1536 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1542 return if $self->hide;
1543 my $conf = $self->conf;
1545 my( $template, $notice_name );
1548 $template = $opt->{'template'} || '';
1549 $notice_name = $opt->{'notice_name'} || 'Invoice';
1551 $template = scalar(@_) ? shift : '';
1552 $notice_name = 'Invoice';
1555 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1556 unless $conf->exists('invoice_latex');
1558 my $dialstring = $self->cust_main->getfield('fax');
1562 'template' => $template,
1563 'notice_name' => $notice_name,
1566 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1567 'dialstring' => $dialstring,
1569 die $error if $error;
1573 =item batch_invoice [ HASHREF ]
1575 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1576 isn't an open batch, one will be created.
1581 my ($self, $opt) = @_;
1582 my $bill_batch = $self->get_open_bill_batch;
1583 my $cust_bill_batch = FS::cust_bill_batch->new({
1584 batchnum => $bill_batch->batchnum,
1585 invnum => $self->invnum,
1587 return $cust_bill_batch->insert($opt);
1590 =item get_open_batch
1592 Returns the currently open batch as an FS::bill_batch object, creating a new
1593 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1598 sub get_open_bill_batch {
1600 my $conf = $self->conf;
1601 my $hashref = { status => 'O' };
1602 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1603 ? $self->cust_main->agentnum
1605 my $batch = qsearchs('bill_batch', $hashref);
1606 return $batch if $batch;
1607 $batch = FS::bill_batch->new($hashref);
1608 my $error = $batch->insert;
1609 die $error if $error;
1613 =item ftp_invoice [ TEMPLATENAME ]
1615 Sends this invoice data via FTP.
1617 TEMPLATENAME is unused?
1623 my $conf = $self->conf;
1624 my $template = scalar(@_) ? shift : '';
1627 'protocol' => 'ftp',
1628 'server' => $conf->config('cust_bill-ftpserver'),
1629 'username' => $conf->config('cust_bill-ftpusername'),
1630 'password' => $conf->config('cust_bill-ftppassword'),
1631 'dir' => $conf->config('cust_bill-ftpdir'),
1632 'format' => $conf->config('cust_bill-ftpformat'),
1636 =item spool_invoice [ TEMPLATENAME ]
1638 Spools this invoice data (see L<FS::spool_csv>)
1640 TEMPLATENAME is unused?
1646 my $conf = $self->conf;
1647 my $template = scalar(@_) ? shift : '';
1650 'format' => $conf->config('cust_bill-spoolformat'),
1651 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1655 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1657 Like B<send>, but only sends the invoice if it is the newest open invoice for
1662 sub send_if_newest {
1667 grep { $_->owed > 0 }
1668 qsearch('cust_bill', {
1669 'custnum' => $self->custnum,
1670 #'_date' => { op=>'>', value=>$self->_date },
1671 'invnum' => { op=>'>', value=>$self->invnum },
1678 =item send_csv OPTION => VALUE, ...
1680 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1684 protocol - currently only "ftp"
1690 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1691 and YYMMDDHHMMSS is a timestamp.
1693 See L</print_csv> for a description of the output format.
1698 my($self, %opt) = @_;
1702 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1703 mkdir $spooldir, 0700 unless -d $spooldir;
1705 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1706 my $file = "$spooldir/$tracctnum.csv";
1708 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1710 open(CSV, ">$file") or die "can't open $file: $!";
1718 if ( $opt{protocol} eq 'ftp' ) {
1719 eval "use Net::FTP;";
1721 $net = Net::FTP->new($opt{server}) or die @$;
1723 die "unknown protocol: $opt{protocol}";
1726 $net->login( $opt{username}, $opt{password} )
1727 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1729 $net->binary or die "can't set binary mode";
1731 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1733 $net->put($file) or die "can't put $file: $!";
1743 Spools CSV invoice data.
1749 =item format - 'default' or 'billco'
1751 =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>).
1753 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1755 =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.
1762 my($self, %opt) = @_;
1764 my $cust_main = $self->cust_main;
1766 if ( $opt{'dest'} ) {
1767 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1768 $cust_main->invoicing_list;
1769 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1770 || ! keys %invoicing_list;
1773 if ( $opt{'balanceover'} ) {
1775 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1778 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1779 mkdir $spooldir, 0700 unless -d $spooldir;
1781 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1785 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1786 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1789 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1791 open(CSV, ">>$file") or die "can't open $file: $!";
1792 flock(CSV, LOCK_EX);
1797 if ( lc($opt{'format'}) eq 'billco' ) {
1799 flock(CSV, LOCK_UN);
1804 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1807 open(CSV,">>$file") or die "can't open $file: $!";
1808 flock(CSV, LOCK_EX);
1814 flock(CSV, LOCK_UN);
1821 =item print_csv OPTION => VALUE, ...
1823 Returns CSV data for this invoice.
1827 format - 'default' or 'billco'
1829 Returns a list consisting of two scalars. The first is a single line of CSV
1830 header information for this invoice. The second is one or more lines of CSV
1831 detail information for this invoice.
1833 If I<format> is not specified or "default", the fields of the CSV file are as
1836 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1840 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1842 B<record_type> is C<cust_bill> for the initial header line only. The
1843 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1844 fields are filled in.
1846 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1847 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1850 =item invnum - invoice number
1852 =item custnum - customer number
1854 =item _date - invoice date
1856 =item charged - total invoice amount
1858 =item first - customer first name
1860 =item last - customer first name
1862 =item company - company name
1864 =item address1 - address line 1
1866 =item address2 - address line 1
1876 =item pkg - line item description
1878 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1880 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1882 =item sdate - start date for recurring fee
1884 =item edate - end date for recurring fee
1888 If I<format> is "billco", the fields of the header CSV file are as follows:
1890 +-------------------------------------------------------------------+
1891 | FORMAT HEADER FILE |
1892 |-------------------------------------------------------------------|
1893 | Field | Description | Name | Type | Width |
1894 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1895 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1896 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1897 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1898 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1899 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1900 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1901 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1902 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1903 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1904 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1905 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1906 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1907 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1908 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1909 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1910 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1911 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1912 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1913 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1914 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1915 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1916 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1917 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1918 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1919 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1920 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1921 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1922 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1923 +-------+-------------------------------+------------+------+-------+
1925 If I<format> is "billco", the fields of the detail CSV file are as follows:
1927 FORMAT FOR DETAIL FILE
1929 Field | Description | Name | Type | Width
1930 1 | N/A-Leave Empty | RC | CHAR | 2
1931 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1932 3 | Account Number | TRACCTNUM | CHAR | 15
1933 4 | Invoice Number | TRINVOICE | CHAR | 15
1934 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1935 6 | Transaction Detail | DETAILS | CHAR | 100
1936 7 | Amount | AMT | NUM* | 9
1937 8 | Line Format Control** | LNCTRL | CHAR | 2
1938 9 | Grouping Code | GROUP | CHAR | 2
1939 10 | User Defined | ACCT CODE | CHAR | 15
1944 my($self, %opt) = @_;
1946 eval "use Text::CSV_XS";
1949 my $cust_main = $self->cust_main;
1951 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1953 if ( lc($opt{'format'}) eq 'billco' ) {
1956 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1958 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1960 my( $previous_balance, @unused ) = $self->previous; #previous balance
1962 my $pmt_cr_applied = 0;
1963 $pmt_cr_applied += $_->{'amount'}
1964 foreach ( $self->_items_payments, $self->_items_credits ) ;
1966 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1969 '', # 1 | N/A-Leave Empty CHAR 2
1970 '', # 2 | N/A-Leave Empty CHAR 15
1971 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1972 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1973 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1974 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1975 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1976 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1977 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1978 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1979 '', # 10 | Ancillary Billing Information CHAR 30
1980 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1981 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1984 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1987 $duedate, # 14 | Bill Due Date CHAR 10
1989 $previous_balance, # 15 | Previous Balance NUM* 9
1990 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1991 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1992 $totaldue, # 18 | Total Amt Due NUM* 9
1993 $totaldue, # 19 | Total Amt Due NUM* 9
1994 '', # 20 | 30 Day Aging NUM* 9
1995 '', # 21 | 60 Day Aging NUM* 9
1996 '', # 22 | 90 Day Aging NUM* 9
1997 'N', # 23 | Y/N CHAR 1
1998 '', # 24 | Remittance automation CHAR 100
1999 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2000 $self->custnum, # 26 | Customer Reference Number CHAR 15
2001 '0', # 27 | Federal Tax*** NUM* 9
2002 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2003 '0', # 29 | Other Taxes & Fees*** NUM* 9
2012 time2str("%x", $self->_date),
2013 sprintf("%.2f", $self->charged),
2014 ( map { $cust_main->getfield($_) }
2015 qw( first last company address1 address2 city state zip country ) ),
2017 ) or die "can't create csv";
2020 my $header = $csv->string. "\n";
2023 if ( lc($opt{'format'}) eq 'billco' ) {
2026 foreach my $item ( $self->_items_pkg ) {
2029 '', # 1 | N/A-Leave Empty CHAR 2
2030 '', # 2 | N/A-Leave Empty CHAR 15
2031 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2032 $self->invnum, # 4 | Invoice Number CHAR 15
2033 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2034 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2035 $item->{'amount'}, # 7 | Amount NUM* 9
2036 '', # 8 | Line Format Control** CHAR 2
2037 '', # 9 | Grouping Code CHAR 2
2038 '', # 10 | User Defined CHAR 15
2041 $detail .= $csv->string. "\n";
2047 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2049 my($pkg, $setup, $recur, $sdate, $edate);
2050 if ( $cust_bill_pkg->pkgnum ) {
2052 ($pkg, $setup, $recur, $sdate, $edate) = (
2053 $cust_bill_pkg->part_pkg->pkg,
2054 ( $cust_bill_pkg->setup != 0
2055 ? sprintf("%.2f", $cust_bill_pkg->setup )
2057 ( $cust_bill_pkg->recur != 0
2058 ? sprintf("%.2f", $cust_bill_pkg->recur )
2060 ( $cust_bill_pkg->sdate
2061 ? time2str("%x", $cust_bill_pkg->sdate)
2063 ($cust_bill_pkg->edate
2064 ?time2str("%x", $cust_bill_pkg->edate)
2068 } else { #pkgnum tax
2069 next unless $cust_bill_pkg->setup != 0;
2070 $pkg = $cust_bill_pkg->desc;
2071 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2072 ( $sdate, $edate ) = ( '', '' );
2078 ( map { '' } (1..11) ),
2079 ($pkg, $setup, $recur, $sdate, $edate)
2080 ) or die "can't create csv";
2082 $detail .= $csv->string. "\n";
2088 ( $header, $detail );
2094 Pays this invoice with a compliemntary payment. If there is an error,
2095 returns the error, otherwise returns false.
2101 my $cust_pay = new FS::cust_pay ( {
2102 'invnum' => $self->invnum,
2103 'paid' => $self->owed,
2106 'payinfo' => $self->cust_main->payinfo,
2114 Attempts to pay this invoice with a credit card payment via a
2115 Business::OnlinePayment realtime gateway. See
2116 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2117 for supported processors.
2123 $self->realtime_bop( 'CC', @_ );
2128 Attempts to pay this invoice with an electronic check (ACH) payment via a
2129 Business::OnlinePayment realtime gateway. See
2130 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2131 for supported processors.
2137 $self->realtime_bop( 'ECHECK', @_ );
2142 Attempts to pay this invoice with phone bill (LEC) payment via a
2143 Business::OnlinePayment realtime gateway. See
2144 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2145 for supported processors.
2151 $self->realtime_bop( 'LEC', @_ );
2155 my( $self, $method ) = (shift,shift);
2156 my $conf = $self->conf;
2159 my $cust_main = $self->cust_main;
2160 my $balance = $cust_main->balance;
2161 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2162 $amount = sprintf("%.2f", $amount);
2163 return "not run (balance $balance)" unless $amount > 0;
2165 my $description = 'Internet Services';
2166 if ( $conf->exists('business-onlinepayment-description') ) {
2167 my $dtempl = $conf->config('business-onlinepayment-description');
2169 my $agent_obj = $cust_main->agent
2170 or die "can't retreive agent for $cust_main (agentnum ".
2171 $cust_main->agentnum. ")";
2172 my $agent = $agent_obj->agent;
2173 my $pkgs = join(', ',
2174 map { $_->part_pkg->pkg }
2175 grep { $_->pkgnum } $self->cust_bill_pkg
2177 $description = eval qq("$dtempl");
2180 $cust_main->realtime_bop($method, $amount,
2181 'description' => $description,
2182 'invnum' => $self->invnum,
2183 #this didn't do what we want, it just calls apply_payments_and_credits
2185 'apply_to_invoice' => 1,
2188 #this changes application behavior: auto payments
2189 #triggered against a specific invoice are now applied
2190 #to that invoice instead of oldest open.
2196 =item batch_card OPTION => VALUE...
2198 Adds a payment for this invoice to the pending credit card batch (see
2199 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2200 runs the payment using a realtime gateway.
2205 my ($self, %options) = @_;
2206 my $cust_main = $self->cust_main;
2208 $options{invnum} = $self->invnum;
2210 $cust_main->batch_card(%options);
2213 sub _agent_template {
2215 $self->cust_main->agent_template;
2218 sub _agent_invoice_from {
2220 $self->cust_main->agent_invoice_from;
2223 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2225 Returns an text invoice, as a list of lines.
2227 Options can be passed as a hashref (recommended) or as a list of time, template
2228 and then any key/value pairs for any other options.
2230 I<time>, if specified, is used to control the printing of overdue messages. The
2231 default is now. It isn't the date of the invoice; that's the `_date' field.
2232 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2233 L<Time::Local> and L<Date::Parse> for conversion functions.
2235 I<template>, if specified, is the name of a suffix for alternate invoices.
2237 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2243 my( $today, $template, %opt );
2245 %opt = %{ shift() };
2246 $today = delete($opt{'time'}) || '';
2247 $template = delete($opt{template}) || '';
2249 ( $today, $template, %opt ) = @_;
2252 my %params = ( 'format' => 'template' );
2253 $params{'time'} = $today if $today;
2254 $params{'template'} = $template if $template;
2255 $params{$_} = $opt{$_}
2256 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2258 $self->print_generic( %params );
2261 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2263 Internal method - returns a filename of a filled-in LaTeX template for this
2264 invoice (Note: add ".tex" to get the actual filename), and a filename of
2265 an associated logo (with the .eps extension included).
2267 See print_ps and print_pdf for methods that return PostScript and PDF output.
2269 Options can be passed as a hashref (recommended) or as a list of time, template
2270 and then any key/value pairs for any other options.
2272 I<time>, if specified, is used to control the printing of overdue messages. The
2273 default is now. It isn't the date of the invoice; that's the `_date' field.
2274 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2275 L<Time::Local> and L<Date::Parse> for conversion functions.
2277 I<template>, if specified, is the name of a suffix for alternate invoices.
2279 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2285 my $conf = $self->conf;
2286 my( $today, $template, %opt );
2288 %opt = %{ shift() };
2289 $today = delete($opt{'time'}) || '';
2290 $template = delete($opt{template}) || '';
2292 ( $today, $template, %opt ) = @_;
2295 my %params = ( 'format' => 'latex' );
2296 $params{'time'} = $today if $today;
2297 $params{'template'} = $template if $template;
2298 $params{$_} = $opt{$_}
2299 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2301 $template ||= $self->_agent_template;
2303 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2304 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2308 ) or die "can't open temp file: $!\n";
2310 my $agentnum = $self->cust_main->agentnum;
2312 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2313 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2314 or die "can't write temp file: $!\n";
2316 print $lh $conf->config_binary('logo.eps', $agentnum)
2317 or die "can't write temp file: $!\n";
2320 $params{'logo_file'} = $lh->filename;
2322 if($conf->exists('invoice-barcode')){
2323 my $png_file = $self->invoice_barcode($dir);
2324 my $eps_file = $png_file;
2325 $eps_file =~ s/\.png$/.eps/g;
2326 $png_file =~ /(barcode.*png)/;
2328 $eps_file =~ /(barcode.*eps)/;
2331 my $curr_dir = cwd();
2333 # after painfuly long experimentation, it was determined that sam2p won't
2334 # accept : and other chars in the path, no matter how hard I tried to
2335 # escape them, hence the chdir (and chdir back, just to be safe)
2336 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2337 or die "sam2p failed: $!\n";
2341 $params{'barcode_file'} = $eps_file;
2344 my @filled_in = $self->print_generic( %params );
2346 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2350 ) or die "can't open temp file: $!\n";
2351 binmode($fh, ':utf8'); # language support
2352 print $fh join('', @filled_in );
2355 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2356 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2360 =item invoice_barcode DIR_OR_FALSE
2362 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2363 it is taken as the temp directory where the PNG file will be generated and the
2364 PNG file name is returned. Otherwise, the PNG image itself is returned.
2368 sub invoice_barcode {
2369 my ($self, $dir) = (shift,shift);
2371 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2372 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2373 my $gd = $gdbar->plot(Height => 30);
2376 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2380 ) or die "can't open temp file: $!\n";
2381 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2382 my $png_file = $bh->filename;
2389 =item print_generic OPTION => VALUE ...
2391 Internal method - returns a filled-in template for this invoice as a scalar.
2393 See print_ps and print_pdf for methods that return PostScript and PDF output.
2395 Non optional options include
2396 format - latex, html, template
2398 Optional options include
2400 template - a value used as a suffix for a configuration template
2402 time - a value used to control the printing of overdue messages. The
2403 default is now. It isn't the date of the invoice; that's the `_date' field.
2404 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2405 L<Time::Local> and L<Date::Parse> for conversion functions.
2409 unsquelch_cdr - overrides any per customer cdr squelching when true
2411 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2413 locale - override customer's locale
2417 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2418 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2419 # yes: fixed width/plain text printing will be borked
2421 my( $self, %params ) = @_;
2422 my $conf = $self->conf;
2423 my $today = $params{today} ? $params{today} : time;
2424 warn "$me print_generic called on $self with suffix $params{template}\n"
2427 my $format = $params{format};
2428 die "Unknown format: $format"
2429 unless $format =~ /^(latex|html|template)$/;
2431 my $cust_main = $self->cust_main;
2432 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2433 unless $cust_main->payname
2434 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2436 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2437 'html' => [ '<%=', '%>' ],
2438 'template' => [ '{', '}' ],
2441 warn "$me print_generic creating template\n"
2444 #create the template
2445 my $template = $params{template} ? $params{template} : $self->_agent_template;
2446 my $templatefile = "invoice_$format";
2447 $templatefile .= "_$template"
2448 if length($template) && $conf->exists($templatefile."_$template");
2449 my @invoice_template = map "$_\n", $conf->config($templatefile)
2450 or die "cannot load config data $templatefile";
2453 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2454 #change this to a die when the old code is removed
2455 warn "old-style invoice template $templatefile; ".
2456 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2457 $old_latex = 'true';
2458 @invoice_template = _translate_old_latex_format(@invoice_template);
2461 warn "$me print_generic creating T:T object\n"
2464 my $text_template = new Text::Template(
2466 SOURCE => \@invoice_template,
2467 DELIMITERS => $delimiters{$format},
2470 warn "$me print_generic compiling T:T object\n"
2473 $text_template->compile()
2474 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2477 # additional substitution could possibly cause breakage in existing templates
2478 my %convert_maps = (
2480 'notes' => sub { map "$_", @_ },
2481 'footer' => sub { map "$_", @_ },
2482 'smallfooter' => sub { map "$_", @_ },
2483 'returnaddress' => sub { map "$_", @_ },
2484 'coupon' => sub { map "$_", @_ },
2485 'summary' => sub { map "$_", @_ },
2491 s/%%(.*)$/<!-- $1 -->/g;
2492 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2493 s/\\begin\{enumerate\}/<ol>/g;
2495 s/\\end\{enumerate\}/<\/ol>/g;
2496 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2505 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2507 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2512 s/\\\\\*?\s*$/<BR>/;
2513 s/\\hyphenation\{[\w\s\-]+}//;
2518 'coupon' => sub { "" },
2519 'summary' => sub { "" },
2526 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2527 s/\\begin\{enumerate\}//g;
2529 s/\\end\{enumerate\}//g;
2530 s/\\textbf\{(.*)\}/$1/g;
2537 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2539 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2544 s/\\\\\*?\s*$/\n/; # dubious
2545 s/\\hyphenation\{[\w\s\-]+}//;
2549 'coupon' => sub { "" },
2550 'summary' => sub { "" },
2555 # hashes for differing output formats
2556 my %nbsps = ( 'latex' => '~',
2557 'html' => '', # '&nbps;' would be nice
2558 'template' => '', # not used
2560 my $nbsp = $nbsps{$format};
2562 my %escape_functions = ( 'latex' => \&_latex_escape,
2563 'html' => \&_html_escape_nbsp,#\&encode_entities,
2564 'template' => sub { shift },
2566 my $escape_function = $escape_functions{$format};
2567 my $escape_function_nonbsp = ($format eq 'html')
2568 ? \&_html_escape : $escape_function;
2570 my %date_formats = ( 'latex' => $date_format_long,
2571 'html' => $date_format_long,
2574 $date_formats{'html'} =~ s/ / /g;
2576 my $date_format = $date_formats{$format};
2578 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2580 'html' => sub { return '<b>'. shift(). '</b>'
2582 'template' => sub { shift },
2584 my $embolden_function = $embolden_functions{$format};
2586 my %newline_tokens = ( 'latex' => '\\\\',
2590 my $newline_token = $newline_tokens{$format};
2592 warn "$me generating template variables\n"
2595 # generate template variables
2598 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2602 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2608 $returnaddress = join("\n",
2609 $conf->config_orbase("invoice_${format}returnaddress", $template)
2612 } elsif ( grep /\S/,
2613 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2615 my $convert_map = $convert_maps{$format}{'returnaddress'};
2618 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2623 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2625 my $convert_map = $convert_maps{$format}{'returnaddress'};
2626 $returnaddress = join( "\n", &$convert_map(
2627 map { s/( {2,})/'~' x length($1)/eg;
2631 ( $conf->config('company_name', $self->cust_main->agentnum),
2632 $conf->config('company_address', $self->cust_main->agentnum),
2639 my $warning = "Couldn't find a return address; ".
2640 "do you need to set the company_address configuration value?";
2642 $returnaddress = $nbsp;
2643 #$returnaddress = $warning;
2647 warn "$me generating invoice data\n"
2650 my $agentnum = $self->cust_main->agentnum;
2652 my %invoice_data = (
2655 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2656 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2657 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2658 'returnaddress' => $returnaddress,
2659 'agent' => &$escape_function($cust_main->agent->agent),
2662 'invnum' => $self->invnum,
2663 'date' => time2str($date_format, $self->_date),
2664 'today' => time2str($date_format_long, $today),
2665 'terms' => $self->terms,
2666 'template' => $template, #params{'template'},
2667 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2668 'current_charges' => sprintf("%.2f", $self->charged),
2669 'duedate' => $self->due_date2str($rdate_format), #date_format?
2672 'custnum' => $cust_main->display_custnum,
2673 'agent_custid' => &$escape_function($cust_main->agent_custid),
2674 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2675 payname company address1 address2 city state zip fax
2679 'ship_enable' => $conf->exists('invoice-ship_address'),
2680 'unitprices' => $conf->exists('invoice-unitprice'),
2681 'smallernotes' => $conf->exists('invoice-smallernotes'),
2682 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2683 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2685 #layout info -- would be fancy to calc some of this and bury the template
2687 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2688 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2689 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2690 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2691 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2692 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2693 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2694 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2695 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2696 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2698 # better hang on to conf_dir for a while (for old templates)
2699 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2701 #these are only used when doing paged plaintext
2708 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2709 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2710 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2711 # eval to avoid death for unimplemented languages
2712 my $dh = eval { Date::Language->new($info{'name'}) } ||
2713 Date::Language->new(); # fall back to English
2714 # prototype here to silence warnings
2715 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2716 # eventually use this date handle everywhere in here, too
2718 my $min_sdate = 999999999999;
2720 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2721 next unless $cust_bill_pkg->pkgnum > 0;
2722 $min_sdate = $cust_bill_pkg->sdate
2723 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2724 $max_edate = $cust_bill_pkg->edate
2725 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2728 $invoice_data{'bill_period'} = '';
2729 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2730 . " to " . time2str('%e %h', $max_edate)
2731 if ($max_edate != 0 && $min_sdate != 999999999999);
2733 $invoice_data{finance_section} = '';
2734 if ( $conf->config('finance_pkgclass') ) {
2736 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2737 $invoice_data{finance_section} = $pkg_class->categoryname;
2739 $invoice_data{finance_amount} = '0.00';
2740 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2742 my $countrydefault = $conf->config('countrydefault') || 'US';
2743 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2744 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2745 my $method = $prefix.$_;
2746 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2748 $invoice_data{'ship_country'} = ''
2749 if ( $invoice_data{'ship_country'} eq $countrydefault );
2751 $invoice_data{'cid'} = $params{'cid'}
2754 if ( $cust_main->country eq $countrydefault ) {
2755 $invoice_data{'country'} = '';
2757 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2761 $invoice_data{'address'} = \@address;
2763 $cust_main->payname.
2764 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2765 ? " (P.O. #". $cust_main->payinfo. ")"
2769 push @address, $cust_main->company
2770 if $cust_main->company;
2771 push @address, $cust_main->address1;
2772 push @address, $cust_main->address2
2773 if $cust_main->address2;
2775 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2776 push @address, $invoice_data{'country'}
2777 if $invoice_data{'country'};
2779 while (scalar(@address) < 5);
2781 $invoice_data{'logo_file'} = $params{'logo_file'}
2782 if $params{'logo_file'};
2783 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2784 if $params{'barcode_file'};
2785 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2786 if $params{'barcode_img'};
2787 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2788 if $params{'barcode_cid'};
2790 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2791 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2792 #my $balance_due = $self->owed + $pr_total - $cr_total;
2793 my $balance_due = $self->owed + $pr_total;
2795 # the customer's current balance as shown on the invoice before this one
2796 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2798 # the change in balance from that invoice to this one
2799 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2801 # the sum of amount owed on all previous invoices
2802 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2804 # the sum of amount owed on all invoices
2805 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2807 # info from customer's last invoice before this one, for some
2809 $invoice_data{'last_bill'} = {};
2810 my $last_bill = $pr_cust_bill[-1];
2812 $invoice_data{'last_bill'} = {
2813 '_date' => $last_bill->_date, #unformatted
2814 # all we need for now
2818 my $summarypage = '';
2819 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2822 $invoice_data{'summarypage'} = $summarypage;
2824 warn "$me substituting variables in notes, footer, smallfooter\n"
2827 my @include = (qw( notes footer smallfooter ));
2828 push @include, 'coupon' unless $params{'no_coupon'};
2829 foreach my $include (@include) {
2831 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2834 if ( $conf->exists($inc_file, $agentnum)
2835 && length( $conf->config($inc_file, $agentnum) ) ) {
2837 @inc_src = $conf->config($inc_file, $agentnum);
2841 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2843 my $convert_map = $convert_maps{$format}{$include};
2845 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2846 s/--\@\]/$delimiters{$format}[1]/g;
2849 &$convert_map( $conf->config($inc_file, $agentnum) );
2853 my $inc_tt = new Text::Template (
2855 SOURCE => [ map "$_\n", @inc_src ],
2856 DELIMITERS => $delimiters{$format},
2857 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2859 unless ( $inc_tt->compile() ) {
2860 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2861 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2865 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2867 $invoice_data{$include} =~ s/\n+$//
2868 if ($format eq 'latex');
2871 # let invoices use either of these as needed
2872 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2873 ? $cust_main->payinfo : '';
2874 $invoice_data{'po_line'} =
2875 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2876 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2879 my %money_chars = ( 'latex' => '',
2880 'html' => $conf->config('money_char') || '$',
2883 my $money_char = $money_chars{$format};
2885 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2886 'html' => $conf->config('money_char') || '$',
2889 my $other_money_char = $other_money_chars{$format};
2890 $invoice_data{'dollar'} = $other_money_char;
2892 my @detail_items = ();
2893 my @total_items = ();
2897 $invoice_data{'detail_items'} = \@detail_items;
2898 $invoice_data{'total_items'} = \@total_items;
2899 $invoice_data{'buf'} = \@buf;
2900 $invoice_data{'sections'} = \@sections;
2902 warn "$me generating sections\n"
2905 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2906 'subtotal' => $other_money_char.
2907 sprintf('%.2f', $pr_total),
2908 'summarized' => '', #why? $summarypage ? 'Y' : '',
2910 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2911 join(' / ', map { $cust_main->balance_date_range(@$_) }
2912 $self->_prior_month30s
2914 if $conf->exists('invoice_include_aging');
2917 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2918 'subtotal' => $taxtotal, # adjusted below
2920 my $tax_weight = _pkg_category($tax_section->{description})
2921 ? _pkg_category($tax_section->{description})->weight
2923 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2924 $tax_section->{'sort_weight'} = $tax_weight;
2927 my $adjusttotal = 0;
2928 my $adjust_section = { 'description' =>
2929 $self->mt('Credits, Payments, and Adjustments'),
2930 'subtotal' => 0, # adjusted below
2932 my $adjust_weight = _pkg_category($adjust_section->{description})
2933 ? _pkg_category($adjust_section->{description})->weight
2935 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2936 $adjust_section->{'sort_weight'} = $adjust_weight;
2938 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2939 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2940 $invoice_data{'multisection'} = $multisection;
2941 my $late_sections = [];
2942 my $extra_sections = [];
2943 my $extra_lines = ();
2944 if ( $multisection ) {
2945 ($extra_sections, $extra_lines) =
2946 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2947 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2949 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2951 push @detail_items, @$extra_lines if $extra_lines;
2953 $self->_items_sections( $late_sections, # this could stand a refactor
2955 $escape_function_nonbsp,
2959 if ($conf->exists('svc_phone_sections')) {
2960 my ($phone_sections, $phone_lines) =
2961 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2962 push @{$late_sections}, @$phone_sections;
2963 push @detail_items, @$phone_lines;
2965 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2966 my ($accountcode_section, $accountcode_lines) =
2967 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2968 if ( scalar(@$accountcode_lines) ) {
2969 push @{$late_sections}, $accountcode_section;
2970 push @detail_items, @$accountcode_lines;
2973 } else {# not multisection
2974 # make a default section
2975 push @sections, { 'description' => '', 'subtotal' => '',
2976 'no_subtotal' => 1 };
2977 # and calculate the finance charge total, since it won't get done otherwise.
2978 # XXX possibly other totals?
2979 # XXX possibly finance_pkgclass should not be used in this manner?
2980 if ( $conf->exists('finance_pkgclass') ) {
2981 my @finance_charges;
2982 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2983 if ( grep { $_->section eq $invoice_data{finance_section} }
2984 $cust_bill_pkg->cust_bill_pkg_display ) {
2985 # I think these are always setup fees, but just to be sure...
2986 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2989 $invoice_data{finance_amount} =
2990 sprintf('%.2f', sum( @finance_charges ) || 0);
2994 unless ( $conf->exists('disable_previous_balance')
2995 || $conf->exists('previous_balance-summary_only')
2999 warn "$me adding previous balances\n"
3002 foreach my $line_item ( $self->_items_previous ) {
3005 ext_description => [],
3007 $detail->{'ref'} = $line_item->{'pkgnum'};
3008 $detail->{'quantity'} = 1;
3009 $detail->{'section'} = $previous_section;
3010 $detail->{'description'} = &$escape_function($line_item->{'description'});
3011 if ( exists $line_item->{'ext_description'} ) {
3012 @{$detail->{'ext_description'}} = map {
3013 &$escape_function($_);
3014 } @{$line_item->{'ext_description'}};
3016 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3017 $line_item->{'amount'};
3018 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3020 push @detail_items, $detail;
3021 push @buf, [ $detail->{'description'},
3022 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3028 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
3029 push @buf, ['','-----------'];
3030 push @buf, [ $self->mt('Total Previous Balance'),
3031 $money_char. sprintf("%10.2f", $pr_total) ];
3035 if ( $conf->exists('svc_phone-did-summary') ) {
3036 warn "$me adding DID summary\n"
3039 my ($didsummary,$minutes) = $self->_did_summary;
3040 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3042 { 'description' => $didsummary_desc,
3043 'ext_description' => [ $didsummary, $minutes ],
3047 foreach my $section (@sections, @$late_sections) {
3049 warn "$me adding section \n". Dumper($section)
3052 # begin some normalization
3053 $section->{'subtotal'} = $section->{'amount'}
3055 && !exists($section->{subtotal})
3056 && exists($section->{amount});
3058 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3059 if ( $invoice_data{finance_section} &&
3060 $section->{'description'} eq $invoice_data{finance_section} );
3062 $section->{'subtotal'} = $other_money_char.
3063 sprintf('%.2f', $section->{'subtotal'})
3066 # continue some normalization
3067 $section->{'amount'} = $section->{'subtotal'}
3071 if ( $section->{'description'} ) {
3072 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3077 warn "$me setting options\n"
3080 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3082 $options{'section'} = $section if $multisection;
3083 $options{'format'} = $format;
3084 $options{'escape_function'} = $escape_function;
3085 $options{'no_usage'} = 1 unless $unsquelched;
3086 $options{'unsquelched'} = $unsquelched;
3087 $options{'summary_page'} = $summarypage;
3088 $options{'skip_usage'} =
3089 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3090 $options{'multilocation'} = $multilocation;
3091 $options{'multisection'} = $multisection;
3093 warn "$me searching for line items\n"
3096 foreach my $line_item ( $self->_items_pkg(%options) ) {
3098 warn "$me adding line item $line_item\n"
3102 ext_description => [],
3104 $detail->{'ref'} = $line_item->{'pkgnum'};
3105 $detail->{'quantity'} = $line_item->{'quantity'};
3106 $detail->{'section'} = $section;
3107 $detail->{'description'} = &$escape_function($line_item->{'description'});
3108 if ( exists $line_item->{'ext_description'} ) {
3109 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3111 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3112 $line_item->{'amount'};
3113 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3114 $line_item->{'unit_amount'};
3115 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3117 $detail->{'sdate'} = $line_item->{'sdate'};
3118 $detail->{'edate'} = $line_item->{'edate'};
3119 $detail->{'seconds'} = $line_item->{'seconds'};
3121 push @detail_items, $detail;
3122 push @buf, ( [ $detail->{'description'},
3123 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3125 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3129 if ( $section->{'description'} ) {
3130 push @buf, ( ['','-----------'],
3131 [ $section->{'description'}. ' sub-total',
3132 $section->{'subtotal'} # already formatted this
3141 $invoice_data{current_less_finance} =
3142 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3144 if ( $multisection && !$conf->exists('disable_previous_balance')
3145 || $conf->exists('previous_balance-summary_only') )
3147 unshift @sections, $previous_section if $pr_total;
3150 warn "$me adding taxes\n"
3153 foreach my $tax ( $self->_items_tax ) {
3155 $taxtotal += $tax->{'amount'};
3157 my $description = &$escape_function( $tax->{'description'} );
3158 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3160 if ( $multisection ) {
3162 my $money = $old_latex ? '' : $money_char;
3163 push @detail_items, {
3164 ext_description => [],
3167 description => $description,
3168 amount => $money. $amount,
3170 section => $tax_section,
3175 push @total_items, {
3176 'total_item' => $description,
3177 'total_amount' => $other_money_char. $amount,
3182 push @buf,[ $description,
3183 $money_char. $amount,
3190 $total->{'total_item'} = $self->mt('Sub-total');
3191 $total->{'total_amount'} =
3192 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3194 if ( $multisection ) {
3195 $tax_section->{'subtotal'} = $other_money_char.
3196 sprintf('%.2f', $taxtotal);
3197 $tax_section->{'pretotal'} = 'New charges sub-total '.
3198 $total->{'total_amount'};
3199 push @sections, $tax_section if $taxtotal;
3201 unshift @total_items, $total;
3204 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3206 push @buf,['','-----------'];
3207 push @buf,[$self->mt(
3208 $conf->exists('disable_previous_balance')
3210 : 'Total New Charges'
3212 $money_char. sprintf("%10.2f",$self->charged) ];
3218 $item = $conf->config('previous_balance-exclude_from_total')
3219 || 'Total New Charges'
3220 if $conf->exists('previous_balance-exclude_from_total');
3221 my $amount = $self->charged +
3222 ( $conf->exists('disable_previous_balance') ||
3223 $conf->exists('previous_balance-exclude_from_total')
3227 $total->{'total_item'} = &$embolden_function($self->mt($item));
3228 $total->{'total_amount'} =
3229 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3230 if ( $multisection ) {
3231 if ( $adjust_section->{'sort_weight'} ) {
3232 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3233 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3235 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3236 $other_money_char. sprintf('%.2f', $self->charged );
3239 push @total_items, $total;
3241 push @buf,['','-----------'];
3244 sprintf( '%10.2f', $amount )
3249 unless ( $conf->exists('disable_previous_balance') ) {
3250 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3253 my $credittotal = 0;
3254 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3257 $total->{'total_item'} = &$escape_function($credit->{'description'});
3258 $credittotal += $credit->{'amount'};
3259 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3260 $adjusttotal += $credit->{'amount'};
3261 if ( $multisection ) {
3262 my $money = $old_latex ? '' : $money_char;
3263 push @detail_items, {
3264 ext_description => [],
3267 description => &$escape_function($credit->{'description'}),
3268 amount => $money. $credit->{'amount'},
3270 section => $adjust_section,
3273 push @total_items, $total;
3277 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3280 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3281 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3285 my $paymenttotal = 0;
3286 foreach my $payment ( $self->_items_payments ) {
3288 $total->{'total_item'} = &$escape_function($payment->{'description'});
3289 $paymenttotal += $payment->{'amount'};
3290 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3291 $adjusttotal += $payment->{'amount'};
3292 if ( $multisection ) {
3293 my $money = $old_latex ? '' : $money_char;
3294 push @detail_items, {
3295 ext_description => [],
3298 description => &$escape_function($payment->{'description'}),
3299 amount => $money. $payment->{'amount'},
3301 section => $adjust_section,
3304 push @total_items, $total;
3306 push @buf, [ $payment->{'description'},
3307 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3310 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3312 if ( $multisection ) {
3313 $adjust_section->{'subtotal'} = $other_money_char.
3314 sprintf('%.2f', $adjusttotal);
3315 push @sections, $adjust_section
3316 unless $adjust_section->{sort_weight};
3319 # create Balance Due message
3322 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3323 $total->{'total_amount'} =
3324 &$embolden_function(
3325 $other_money_char. sprintf('%.2f', $summarypage
3327 $self->billing_balance
3328 : $self->owed + $pr_total
3331 if ( $multisection && !$adjust_section->{sort_weight} ) {
3332 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3333 $total->{'total_amount'};
3335 push @total_items, $total;
3337 push @buf,['','-----------'];
3338 push @buf,[$self->balance_due_msg, $money_char.
3339 sprintf("%10.2f", $balance_due ) ];
3342 if ( $conf->exists('previous_balance-show_credit')
3343 and $cust_main->balance < 0 ) {
3344 my $credit_total = {
3345 'total_item' => &$embolden_function($self->credit_balance_msg),
3346 'total_amount' => &$embolden_function(
3347 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3350 if ( $multisection ) {
3351 $adjust_section->{'posttotal'} .= $newline_token .
3352 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3355 push @total_items, $credit_total;
3357 push @buf,['','-----------'];
3358 push @buf,[$self->credit_balance_msg, $money_char.
3359 sprintf("%10.2f", -$cust_main->balance ) ];
3363 if ( $multisection ) {
3364 if ($conf->exists('svc_phone_sections')) {
3366 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3367 $total->{'total_amount'} =
3368 &$embolden_function(
3369 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3371 my $last_section = pop @sections;
3372 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3373 $total->{'total_amount'};
3374 push @sections, $last_section;
3376 push @sections, @$late_sections
3380 # make a discounts-available section, even without multisection
3381 if ( $conf->exists('discount-show_available')
3382 and my @discounts_avail = $self->_items_discounts_avail ) {
3383 my $discount_section = {
3384 'description' => $self->mt('Discounts Available'),
3389 push @sections, $discount_section;
3390 push @detail_items, map { +{
3391 'ref' => '', #should this be something else?
3392 'section' => $discount_section,
3393 'description' => &$escape_function( $_->{description} ),
3394 'amount' => $money_char . &$escape_function( $_->{amount} ),
3395 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3396 } } @discounts_avail;
3399 # All sections and items are built; now fill in templates.
3400 my @includelist = ();
3401 push @includelist, 'summary' if $summarypage;
3402 foreach my $include ( @includelist ) {
3404 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3407 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3409 @inc_src = $conf->config($inc_file, $agentnum);
3413 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3415 my $convert_map = $convert_maps{$format}{$include};
3417 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3418 s/--\@\]/$delimiters{$format}[1]/g;
3421 &$convert_map( $conf->config($inc_file, $agentnum) );
3425 my $inc_tt = new Text::Template (
3427 SOURCE => [ map "$_\n", @inc_src ],
3428 DELIMITERS => $delimiters{$format},
3429 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3431 unless ( $inc_tt->compile() ) {
3432 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3433 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3437 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3439 $invoice_data{$include} =~ s/\n+$//
3440 if ($format eq 'latex');
3445 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3446 /invoice_lines\((\d*)\)/;
3447 $invoice_lines += $1 || scalar(@buf);
3450 die "no invoice_lines() functions in template?"
3451 if ( $format eq 'template' && !$wasfunc );
3453 if ($format eq 'template') {
3455 if ( $invoice_lines ) {
3456 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3457 $invoice_data{'total_pages'}++
3458 if scalar(@buf) % $invoice_lines;
3461 #setup subroutine for the template
3462 $invoice_data{invoice_lines} = sub {
3463 my $lines = shift || scalar(@buf);
3475 push @collect, split("\n",
3476 $text_template->fill_in( HASH => \%invoice_data )
3478 $invoice_data{'page'}++;
3480 map "$_\n", @collect;
3482 # this is where we actually create the invoice
3483 warn "filling in template for invoice ". $self->invnum. "\n"
3485 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3488 $text_template->fill_in(HASH => \%invoice_data);
3492 # helper routine for generating date ranges
3493 sub _prior_month30s {
3496 [ 1, 2592000 ], # 0-30 days ago
3497 [ 2592000, 5184000 ], # 30-60 days ago
3498 [ 5184000, 7776000 ], # 60-90 days ago
3499 [ 7776000, 0 ], # 90+ days ago
3502 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3503 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3508 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3510 Returns an postscript invoice, as a scalar.
3512 Options can be passed as a hashref (recommended) or as a list of time, template
3513 and then any key/value pairs for any other options.
3515 I<time> an optional value used to control the printing of overdue messages. The
3516 default is now. It isn't the date of the invoice; that's the `_date' field.
3517 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3518 L<Time::Local> and L<Date::Parse> for conversion functions.
3520 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3527 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3528 my $ps = generate_ps($file);
3530 unlink($barcodefile) if $barcodefile;
3535 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3537 Returns an PDF invoice, as a scalar.
3539 Options can be passed as a hashref (recommended) or as a list of time, template
3540 and then any key/value pairs for any other options.
3542 I<time> an optional value used to control the printing of overdue messages. The
3543 default is now. It isn't the date of the invoice; that's the `_date' field.
3544 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3545 L<Time::Local> and L<Date::Parse> for conversion functions.
3547 I<template>, if specified, is the name of a suffix for alternate invoices.
3549 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3556 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3557 my $pdf = generate_pdf($file);
3559 unlink($barcodefile) if $barcodefile;
3564 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3566 Returns an HTML invoice, as a scalar.
3568 I<time> an optional value used to control the printing of overdue messages. The
3569 default is now. It isn't the date of the invoice; that's the `_date' field.
3570 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3571 L<Time::Local> and L<Date::Parse> for conversion functions.
3573 I<template>, if specified, is the name of a suffix for alternate invoices.
3575 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3577 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3578 when emailing the invoice as part of a multipart/related MIME email.
3586 %params = %{ shift() };
3588 $params{'time'} = shift;
3589 $params{'template'} = shift;
3590 $params{'cid'} = shift;
3593 $params{'format'} = 'html';
3595 $self->print_generic( %params );
3598 # quick subroutine for print_latex
3600 # There are ten characters that LaTeX treats as special characters, which
3601 # means that they do not simply typeset themselves:
3602 # # $ % & ~ _ ^ \ { }
3604 # TeX ignores blanks following an escaped character; if you want a blank (as
3605 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3609 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3610 $value =~ s/([<>])/\$$1\$/g;
3616 encode_entities($value);
3620 sub _html_escape_nbsp {
3621 my $value = _html_escape(shift);
3622 $value =~ s/ +/ /g;
3626 #utility methods for print_*
3628 sub _translate_old_latex_format {
3629 warn "_translate_old_latex_format called\n"
3636 if ( $line =~ /^%%Detail\s*$/ ) {
3638 push @template, q![@--!,
3639 q! foreach my $_tr_line (@detail_items) {!,
3640 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3641 q! $_tr_line->{'description'} .= !,
3642 q! "\\tabularnewline\n~~".!,
3643 q! join( "\\tabularnewline\n~~",!,
3644 q! @{$_tr_line->{'ext_description'}}!,
3648 while ( ( my $line_item_line = shift )
3649 !~ /^%%EndDetail\s*$/ ) {
3650 $line_item_line =~ s/'/\\'/g; # nice LTS
3651 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3652 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3653 push @template, " \$OUT .= '$line_item_line';";
3656 push @template, '}',
3659 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3661 push @template, '[@--',
3662 ' foreach my $_tr_line (@total_items) {';
3664 while ( ( my $total_item_line = shift )
3665 !~ /^%%EndTotalDetails\s*$/ ) {
3666 $total_item_line =~ s/'/\\'/g; # nice LTS
3667 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3668 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3669 push @template, " \$OUT .= '$total_item_line';";
3672 push @template, '}',
3676 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3677 push @template, $line;
3683 warn "$_\n" foreach @template;
3691 my $conf = $self->conf;
3693 #check for an invoice-specific override
3694 return $self->invoice_terms if $self->invoice_terms;
3696 #check for a customer- specific override
3697 my $cust_main = $self->cust_main;
3698 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3700 #use configured default
3701 $conf->config('invoice_default_terms') || '';
3707 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3708 $duedate = $self->_date() + ( $1 * 86400 );
3715 $self->due_date ? time2str(shift, $self->due_date) : '';
3718 sub balance_due_msg {
3720 my $msg = $self->mt('Balance Due');
3721 return $msg unless $self->terms;
3722 if ( $self->due_date ) {
3723 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3724 $self->due_date2str($date_format);
3725 } elsif ( $self->terms ) {
3726 $msg .= ' - '. $self->terms;
3731 sub balance_due_date {
3733 my $conf = $self->conf;
3735 if ( $conf->exists('invoice_default_terms')
3736 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3737 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3742 sub credit_balance_msg {
3744 $self->mt('Credit Balance Remaining')
3747 =item invnum_date_pretty
3749 Returns a string with the invoice number and date, for example:
3750 "Invoice #54 (3/20/2008)"
3754 sub invnum_date_pretty {
3756 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3761 Returns a string with the date, for example: "3/20/2008"
3767 time2str($date_format, $self->_date);
3770 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3772 Generate section information for all items appearing on this invoice.
3773 This will only be called for multi-section invoices.
3775 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3776 related display records (L<FS::cust_bill_pkg_display>) and organize
3777 them into two groups ("early" and "late" according to whether they come
3778 before or after the total), then into sections. A subtotal is calculated
3781 Section descriptions are returned in sort weight order. Each consists
3782 of a hash containing:
3784 description: the package category name, escaped
3785 subtotal: the total charges in that section
3786 tax_section: a flag indicating that the section contains only tax charges
3787 summarized: same as tax_section, for some reason
3788 sort_weight: the package category's sort weight
3790 If 'condense' is set on the display record, it also contains everything
3791 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3792 coderefs to generate parts of the invoice. This is not advised.
3796 LATE: an arrayref to push the "late" section hashes onto. The "early"
3797 group is simply returned from the method.
3799 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3800 Turning this on has the following effects:
3801 - Ignores display items with the 'summary' flag.
3802 - Combines all items into the "early" group.
3803 - Creates sections for all non-disabled package categories, even if they
3804 have no charges on this invoice, as well as a section with no name.
3806 ESCAPE: an escape function to use for section titles.
3808 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3809 sorted list. If there are any of these, section subtotals exclude
3812 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3813 passed through to C<_condense_section()>.
3817 use vars qw(%pkg_category_cache);
3818 sub _items_sections {
3821 my $summarypage = shift;
3823 my $extra_sections = shift;
3827 my %late_subtotal = ();
3830 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3833 my $usage = $cust_bill_pkg->usage;
3835 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3836 next if ( $display->summary && $summarypage );
3838 my $section = $display->section;
3839 my $type = $display->type;
3841 $not_tax{$section} = 1
3842 unless $cust_bill_pkg->pkgnum == 0;
3844 if ( $display->post_total && !$summarypage ) {
3845 if (! $type || $type eq 'S') {
3846 $late_subtotal{$section} += $cust_bill_pkg->setup
3847 if $cust_bill_pkg->setup != 0;
3851 $late_subtotal{$section} += $cust_bill_pkg->recur
3852 if $cust_bill_pkg->recur != 0;
3855 if ($type && $type eq 'R') {
3856 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3857 if $cust_bill_pkg->recur != 0;
3860 if ($type && $type eq 'U') {
3861 $late_subtotal{$section} += $usage
3862 unless scalar(@$extra_sections);
3867 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3869 if (! $type || $type eq 'S') {
3870 $subtotal{$section} += $cust_bill_pkg->setup
3871 if $cust_bill_pkg->setup != 0;
3875 $subtotal{$section} += $cust_bill_pkg->recur
3876 if $cust_bill_pkg->recur != 0;
3879 if ($type && $type eq 'R') {
3880 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3881 if $cust_bill_pkg->recur != 0;
3884 if ($type && $type eq 'U') {
3885 $subtotal{$section} += $usage
3886 unless scalar(@$extra_sections);
3895 %pkg_category_cache = ();
3897 push @$late, map { { 'description' => &{$escape}($_),
3898 'subtotal' => $late_subtotal{$_},
3900 'sort_weight' => ( _pkg_category($_)
3901 ? _pkg_category($_)->weight
3904 ((_pkg_category($_) && _pkg_category($_)->condense)
3905 ? $self->_condense_section($format)
3909 sort _sectionsort keys %late_subtotal;
3912 if ( $summarypage ) {
3913 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3914 map { $_->categoryname } qsearch('pkg_category', {});
3915 push @sections, '' if exists($subtotal{''});
3917 @sections = keys %subtotal;
3920 my @early = map { { 'description' => &{$escape}($_),
3921 'subtotal' => $subtotal{$_},
3922 'summarized' => $not_tax{$_} ? '' : 'Y',
3923 'tax_section' => $not_tax{$_} ? '' : 'Y',
3924 'sort_weight' => ( _pkg_category($_)
3925 ? _pkg_category($_)->weight
3928 ((_pkg_category($_) && _pkg_category($_)->condense)
3929 ? $self->_condense_section($format)
3934 push @early, @$extra_sections if $extra_sections;
3936 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3940 #helper subs for above
3943 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3947 my $categoryname = shift;
3948 $pkg_category_cache{$categoryname} ||=
3949 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3952 my %condensed_format = (
3953 'label' => [ qw( Description Qty Amount ) ],
3955 sub { shift->{description} },
3956 sub { shift->{quantity} },
3957 sub { my($href, %opt) = @_;
3958 ($opt{dollar} || ''). $href->{amount};
3961 'align' => [ qw( l r r ) ],
3962 'span' => [ qw( 5 1 1 ) ], # unitprices?
3963 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3966 sub _condense_section {
3967 my ( $self, $format ) = ( shift, shift );
3969 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3970 qw( description_generator
3973 total_line_generator
3978 sub _condensed_generator_defaults {
3979 my ( $self, $format ) = ( shift, shift );
3980 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3989 sub _condensed_header_generator {
3990 my ( $self, $format ) = ( shift, shift );
3992 my ( $f, $prefix, $suffix, $separator, $column ) =
3993 _condensed_generator_defaults($format);
3995 if ($format eq 'latex') {
3996 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3997 $suffix = "\\\\\n\\hline";
4000 sub { my ($d,$a,$s,$w) = @_;
4001 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4003 } elsif ( $format eq 'html' ) {
4004 $prefix = '<th></th>';
4008 sub { my ($d,$a,$s,$w) = @_;
4009 return qq!<th align="$html_align{$a}">$d</th>!;
4017 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4019 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4022 $prefix. join($separator, @result). $suffix;
4027 sub _condensed_description_generator {
4028 my ( $self, $format ) = ( shift, shift );
4030 my ( $f, $prefix, $suffix, $separator, $column ) =
4031 _condensed_generator_defaults($format);
4033 my $money_char = '$';
4034 if ($format eq 'latex') {
4035 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4037 $separator = " & \n";
4039 sub { my ($d,$a,$s,$w) = @_;
4040 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4042 $money_char = '\\dollar';
4043 }elsif ( $format eq 'html' ) {
4044 $prefix = '"><td align="center"></td>';
4048 sub { my ($d,$a,$s,$w) = @_;
4049 return qq!<td align="$html_align{$a}">$d</td>!;
4051 #$money_char = $conf->config('money_char') || '$';
4052 $money_char = ''; # this is madness
4060 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4062 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4064 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4065 map { $f->{$_}->[$i] } qw(align span width)
4069 $prefix. join( $separator, @result ). $suffix;
4074 sub _condensed_total_generator {
4075 my ( $self, $format ) = ( shift, shift );
4077 my ( $f, $prefix, $suffix, $separator, $column ) =
4078 _condensed_generator_defaults($format);
4081 if ($format eq 'latex') {
4084 $separator = " & \n";
4086 sub { my ($d,$a,$s,$w) = @_;
4087 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4089 }elsif ( $format eq 'html' ) {
4093 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4095 sub { my ($d,$a,$s,$w) = @_;
4096 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4105 # my $r = &{$f->{fields}->[$i]}(@args);
4106 # $r .= ' Total' unless $i;
4108 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4110 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4111 map { $f->{$_}->[$i] } qw(align span width)
4115 $prefix. join( $separator, @result ). $suffix;
4120 =item total_line_generator FORMAT
4122 Returns a coderef used for generation of invoice total line items for this
4123 usage_class. FORMAT is either html or latex
4127 # should not be used: will have issues with hash element names (description vs
4128 # total_item and amount vs total_amount -- another array of functions?
4130 sub _condensed_total_line_generator {
4131 my ( $self, $format ) = ( shift, shift );
4133 my ( $f, $prefix, $suffix, $separator, $column ) =
4134 _condensed_generator_defaults($format);
4137 if ($format eq 'latex') {
4140 $separator = " & \n";
4142 sub { my ($d,$a,$s,$w) = @_;
4143 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4145 }elsif ( $format eq 'html' ) {
4149 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4151 sub { my ($d,$a,$s,$w) = @_;
4152 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4161 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4163 &{$column}( &{$f->{fields}->[$i]}(@args),
4164 map { $f->{$_}->[$i] } qw(align span width)
4168 $prefix. join( $separator, @result ). $suffix;
4173 #sub _items_extra_usage_sections {
4175 # my $escape = shift;
4177 # my %sections = ();
4179 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4180 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4182 # next unless $cust_bill_pkg->pkgnum > 0;
4184 # foreach my $section ( keys %usage_class ) {
4186 # my $usage = $cust_bill_pkg->usage($section);
4188 # next unless $usage && $usage > 0;
4190 # $sections{$section} ||= 0;
4191 # $sections{$section} += $usage;
4197 # map { { 'description' => &{$escape}($_),
4198 # 'subtotal' => $sections{$_},
4199 # 'summarized' => '',
4200 # 'tax_section' => '',
4203 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4207 sub _items_extra_usage_sections {
4209 my $conf = $self->conf;
4217 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4219 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4220 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4221 next unless $cust_bill_pkg->pkgnum > 0;
4223 foreach my $classnum ( keys %usage_class ) {
4224 my $section = $usage_class{$classnum}->classname;
4225 $classnums{$section} = $classnum;
4227 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4228 my $amount = $detail->amount;
4229 next unless $amount && $amount > 0;
4231 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4232 $sections{$section}{amount} += $amount; #subtotal
4233 $sections{$section}{calls}++;
4234 $sections{$section}{duration} += $detail->duration;
4236 my $desc = $detail->regionname;
4237 my $description = $desc;
4238 $description = substr($desc, 0, $maxlength). '...'
4239 if $format eq 'latex' && length($desc) > $maxlength;
4241 $lines{$section}{$desc} ||= {
4242 description => &{$escape}($description),
4243 #pkgpart => $part_pkg->pkgpart,
4244 pkgnum => $cust_bill_pkg->pkgnum,
4249 #unit_amount => $cust_bill_pkg->unitrecur,
4250 quantity => $cust_bill_pkg->quantity,
4251 product_code => 'N/A',
4252 ext_description => [],
4255 $lines{$section}{$desc}{amount} += $amount;
4256 $lines{$section}{$desc}{calls}++;
4257 $lines{$section}{$desc}{duration} += $detail->duration;
4263 my %sectionmap = ();
4264 foreach (keys %sections) {
4265 my $usage_class = $usage_class{$classnums{$_}};
4266 $sectionmap{$_} = { 'description' => &{$escape}($_),
4267 'amount' => $sections{$_}{amount}, #subtotal
4268 'calls' => $sections{$_}{calls},
4269 'duration' => $sections{$_}{duration},
4271 'tax_section' => '',
4272 'sort_weight' => $usage_class->weight,
4273 ( $usage_class->format
4274 ? ( map { $_ => $usage_class->$_($format) }
4275 qw( description_generator header_generator total_generator total_line_generator )
4282 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4286 foreach my $section ( keys %lines ) {
4287 foreach my $line ( keys %{$lines{$section}} ) {
4288 my $l = $lines{$section}{$line};
4289 $l->{section} = $sectionmap{$section};
4290 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4291 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4296 return(\@sections, \@lines);
4302 my $end = $self->_date;
4304 # start at date of previous invoice + 1 second or 0 if no previous invoice
4305 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4306 $start = 0 if !$start;
4309 my $cust_main = $self->cust_main;
4310 my @pkgs = $cust_main->all_pkgs;
4311 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4314 foreach my $pkg ( @pkgs ) {
4315 my @h_cust_svc = $pkg->h_cust_svc($end);
4316 foreach my $h_cust_svc ( @h_cust_svc ) {
4317 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4318 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4320 my $inserted = $h_cust_svc->date_inserted;
4321 my $deleted = $h_cust_svc->date_deleted;
4322 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4324 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4326 # DID either activated or ported in; cannot be both for same DID simultaneously
4327 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4328 && (!$phone_inserted->lnp_status
4329 || $phone_inserted->lnp_status eq ''
4330 || $phone_inserted->lnp_status eq 'native')) {
4333 else { # this one not so clean, should probably move to (h_)svc_phone
4334 my $phone_portedin = qsearchs( 'h_svc_phone',
4335 { 'svcnum' => $h_cust_svc->svcnum,
4336 'lnp_status' => 'portedin' },
4337 FS::h_svc_phone->sql_h_searchs($end),
4339 $num_portedin++ if $phone_portedin;
4342 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4343 if($deleted >= $start && $deleted <= $end && $phone_deleted
4344 && (!$phone_deleted->lnp_status
4345 || $phone_deleted->lnp_status ne 'portingout')) {
4348 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4349 && $phone_deleted->lnp_status
4350 && $phone_deleted->lnp_status eq 'portingout') {
4354 # increment usage minutes
4355 if ( $phone_inserted ) {
4356 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4357 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4360 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4363 # don't look at this service again
4364 push @seen, $h_cust_svc->svcnum;
4368 $minutes = sprintf("%d", $minutes);
4369 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4370 . "$num_deactivated Ported-Out: $num_portedout ",
4371 "Total Minutes: $minutes");
4374 sub _items_accountcode_cdr {
4379 my $section = { 'amount' => 0,
4382 'sort_weight' => '',
4384 'description' => 'Usage by Account Code',
4390 my %accountcodes = ();
4392 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4393 next unless $cust_bill_pkg->pkgnum > 0;
4395 my @header = $cust_bill_pkg->details_header;
4396 next unless scalar(@header);
4397 $section->{'header'} = join(',',@header);
4399 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4401 $section->{'header'} = $detail->formatted('format' => $format)
4402 if($detail->detail eq $section->{'header'});
4404 my $accountcode = $detail->accountcode;
4405 next unless $accountcode;
4407 my $amount = $detail->amount;
4408 next unless $amount && $amount > 0;
4410 $accountcodes{$accountcode} ||= {
4411 description => $accountcode,
4418 product_code => 'N/A',
4419 section => $section,
4420 ext_description => [ $section->{'header'} ],
4424 $section->{'amount'} += $amount;
4425 $accountcodes{$accountcode}{'amount'} += $amount;
4426 $accountcodes{$accountcode}{calls}++;
4427 $accountcodes{$accountcode}{duration} += $detail->duration;
4428 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4432 foreach my $l ( values %accountcodes ) {
4433 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4434 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4435 foreach my $sorted_detail ( @sorted_detail ) {
4436 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4438 delete $l->{detail_temp};
4442 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4444 return ($section,\@sorted_lines);
4447 sub _items_svc_phone_sections {
4449 my $conf = $self->conf;
4457 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4459 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4460 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4462 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4463 next unless $cust_bill_pkg->pkgnum > 0;
4465 my @header = $cust_bill_pkg->details_header;
4466 next unless scalar(@header);
4468 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4470 my $phonenum = $detail->phonenum;
4471 next unless $phonenum;
4473 my $amount = $detail->amount;
4474 next unless $amount && $amount > 0;
4476 $sections{$phonenum} ||= { 'amount' => 0,
4479 'sort_weight' => -1,
4480 'phonenum' => $phonenum,
4482 $sections{$phonenum}{amount} += $amount; #subtotal
4483 $sections{$phonenum}{calls}++;
4484 $sections{$phonenum}{duration} += $detail->duration;
4486 my $desc = $detail->regionname;
4487 my $description = $desc;
4488 $description = substr($desc, 0, $maxlength). '...'
4489 if $format eq 'latex' && length($desc) > $maxlength;
4491 $lines{$phonenum}{$desc} ||= {
4492 description => &{$escape}($description),
4493 #pkgpart => $part_pkg->pkgpart,
4501 product_code => 'N/A',
4502 ext_description => [],
4505 $lines{$phonenum}{$desc}{amount} += $amount;
4506 $lines{$phonenum}{$desc}{calls}++;
4507 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4509 my $line = $usage_class{$detail->classnum}->classname;
4510 $sections{"$phonenum $line"} ||=
4514 'sort_weight' => $usage_class{$detail->classnum}->weight,
4515 'phonenum' => $phonenum,
4516 'header' => [ @header ],
4518 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4519 $sections{"$phonenum $line"}{calls}++;
4520 $sections{"$phonenum $line"}{duration} += $detail->duration;
4522 $lines{"$phonenum $line"}{$desc} ||= {
4523 description => &{$escape}($description),
4524 #pkgpart => $part_pkg->pkgpart,
4532 product_code => 'N/A',
4533 ext_description => [],
4536 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4537 $lines{"$phonenum $line"}{$desc}{calls}++;
4538 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4539 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4540 $detail->formatted('format' => $format);
4545 my %sectionmap = ();
4546 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4547 foreach ( keys %sections ) {
4548 my @header = @{ $sections{$_}{header} || [] };
4550 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4551 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4552 my $usage_class = $summary ? $simple : $usage_simple;
4553 my $ending = $summary ? ' usage charges' : '';
4556 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4558 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4559 'amount' => $sections{$_}{amount}, #subtotal
4560 'calls' => $sections{$_}{calls},
4561 'duration' => $sections{$_}{duration},
4563 'tax_section' => '',
4564 'phonenum' => $sections{$_}{phonenum},
4565 'sort_weight' => $sections{$_}{sort_weight},
4566 'post_total' => $summary, #inspire pagebreak
4568 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4569 qw( description_generator
4572 total_line_generator
4579 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4580 $a->{sort_weight} <=> $b->{sort_weight}
4585 foreach my $section ( keys %lines ) {
4586 foreach my $line ( keys %{$lines{$section}} ) {
4587 my $l = $lines{$section}{$line};
4588 $l->{section} = $sectionmap{$section};
4589 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4590 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4595 if($conf->exists('phone_usage_class_summary')) {
4596 # this only works with Latex
4600 # after this, we'll have only two sections per DID:
4601 # Calls Summary and Calls Detail
4602 foreach my $section ( @sections ) {
4603 if($section->{'post_total'}) {
4604 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4605 $section->{'total_line_generator'} = sub { '' };
4606 $section->{'total_generator'} = sub { '' };
4607 $section->{'header_generator'} = sub { '' };
4608 $section->{'description_generator'} = '';
4609 push @newsections, $section;
4610 my %calls_detail = %$section;
4611 $calls_detail{'post_total'} = '';
4612 $calls_detail{'sort_weight'} = '';
4613 $calls_detail{'description_generator'} = sub { '' };
4614 $calls_detail{'header_generator'} = sub {
4615 return ' & Date/Time & Called Number & Duration & Price'
4616 if $format eq 'latex';
4619 $calls_detail{'description'} = 'Calls Detail: '
4620 . $section->{'phonenum'};
4621 push @newsections, \%calls_detail;
4625 # after this, each usage class is collapsed/summarized into a single
4626 # line under the Calls Summary section
4627 foreach my $newsection ( @newsections ) {
4628 if($newsection->{'post_total'}) { # this means Calls Summary
4629 foreach my $section ( @sections ) {
4630 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4631 && !$section->{'post_total'});
4632 my $newdesc = $section->{'description'};
4633 my $tn = $section->{'phonenum'};
4634 $newdesc =~ s/$tn//g;
4635 my $line = { ext_description => [],
4639 calls => $section->{'calls'},
4640 section => $newsection,
4641 duration => $section->{'duration'},
4642 description => $newdesc,
4643 amount => sprintf("%.2f",$section->{'amount'}),
4644 product_code => 'N/A',
4646 push @newlines, $line;
4651 # after this, Calls Details is populated with all CDRs
4652 foreach my $newsection ( @newsections ) {
4653 if(!$newsection->{'post_total'}) { # this means Calls Details
4654 foreach my $line ( @lines ) {
4655 next unless (scalar(@{$line->{'ext_description'}}) &&
4656 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4658 my @extdesc = @{$line->{'ext_description'}};
4660 foreach my $extdesc ( @extdesc ) {
4661 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4662 push @newextdesc, $extdesc;
4664 $line->{'ext_description'} = \@newextdesc;
4665 $line->{'section'} = $newsection;
4666 push @newlines, $line;
4671 return(\@newsections, \@newlines);
4674 return(\@sections, \@lines);
4678 sub _items { # seems to be unused
4681 #my @display = scalar(@_)
4683 # : qw( _items_previous _items_pkg );
4684 # #: qw( _items_pkg );
4685 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4686 my @display = qw( _items_previous _items_pkg );
4689 foreach my $display ( @display ) {
4690 push @b, $self->$display(@_);
4695 sub _items_previous {
4697 my $conf = $self->conf;
4698 my $cust_main = $self->cust_main;
4699 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4701 foreach ( @pr_cust_bill ) {
4702 my $date = $conf->exists('invoice_show_prior_due_date')
4703 ? 'due '. $_->due_date2str($date_format)
4704 : time2str($date_format, $_->_date);
4706 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4707 #'pkgpart' => 'N/A',
4709 'amount' => sprintf("%.2f", $_->owed),
4715 # 'description' => 'Previous Balance',
4716 # #'pkgpart' => 'N/A',
4717 # 'pkgnum' => 'N/A',
4718 # 'amount' => sprintf("%10.2f", $pr_total ),
4719 # 'ext_description' => [ map {
4720 # "Invoice ". $_->invnum.
4721 # " (". time2str("%x",$_->_date). ") ".
4722 # sprintf("%10.2f", $_->owed)
4723 # } @pr_cust_bill ],
4728 =item _items_pkg [ OPTIONS ]
4730 Return line item hashes for each package item on this invoice. Nearly
4733 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4735 The only OPTIONS accepted is 'section', which may point to a hashref
4736 with a key named 'condensed', which may have a true value. If it
4737 does, this method tries to merge identical items into items with
4738 'quantity' equal to the number of items (not the sum of their
4739 separate quantities, for some reason).
4747 warn "$me _items_pkg searching for all package line items\n"
4750 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4752 warn "$me _items_pkg filtering line items\n"
4754 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4756 if ($options{section} && $options{section}->{condensed}) {
4758 warn "$me _items_pkg condensing section\n"
4762 local $Storable::canonical = 1;
4763 foreach ( @items ) {
4765 delete $item->{ref};
4766 delete $item->{ext_description};
4767 my $key = freeze($item);
4768 $itemshash{$key} ||= 0;
4769 $itemshash{$key} ++; # += $item->{quantity};
4771 @items = sort { $a->{description} cmp $b->{description} }
4772 map { my $i = thaw($_);
4773 $i->{quantity} = $itemshash{$_};
4775 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4781 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4788 return 0 unless $a->itemdesc cmp $b->itemdesc;
4789 return -1 if $b->itemdesc eq 'Tax';
4790 return 1 if $a->itemdesc eq 'Tax';
4791 return -1 if $b->itemdesc eq 'Other surcharges';
4792 return 1 if $a->itemdesc eq 'Other surcharges';
4793 $a->itemdesc cmp $b->itemdesc;
4798 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4799 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4802 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4804 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4805 list of hashrefs describing the line items they generate on the invoice.
4807 OPTIONS may include:
4809 format: the invoice format.
4811 escape_function: the function used to escape strings.
4813 DEPRECATED? (expensive, mostly unused?)
4814 format_function: the function used to format CDRs.
4816 section: a hashref containing 'description'; if this is present,
4817 cust_bill_pkg_display records not belonging to this section are
4820 multisection: a flag indicating that this is a multisection invoice,
4821 which does something complicated.
4823 multilocation: a flag to display the location label for the package.
4825 Returns a list of hashrefs, each of which may contain:
4827 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4828 ext_description, which is an arrayref of detail lines to show below
4833 sub _items_cust_bill_pkg {
4835 my $conf = $self->conf;
4836 my $cust_bill_pkgs = shift;
4839 my $format = $opt{format} || '';
4840 my $escape_function = $opt{escape_function} || sub { shift };
4841 my $format_function = $opt{format_function} || '';
4842 my $no_usage = $opt{no_usage} || '';
4843 my $unsquelched = $opt{unsquelched} || ''; #unused
4844 my $section = $opt{section}->{description} if $opt{section};
4845 my $summary_page = $opt{summary_page} || ''; #unused
4846 my $multilocation = $opt{multilocation} || '';
4847 my $multisection = $opt{multisection} || '';
4848 my $discount_show_always = 0;
4850 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4853 my ($s, $r, $u) = ( undef, undef, undef );
4854 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4857 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4858 if ( $_ && !$cust_bill_pkg->hidden ) {
4859 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4860 $_->{amount} =~ s/^\-0\.00$/0.00/;
4861 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4863 if $_->{amount} != 0
4864 || $discount_show_always
4865 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4866 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4872 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4873 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4876 foreach my $display ( grep { defined($section)
4877 ? $_->section eq $section
4880 #grep { !$_->summary || !$summary_page } # bunk!
4881 grep { !$_->summary || $multisection }
4882 $cust_bill_pkg->cust_bill_pkg_display
4886 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4887 $display->billpkgdisplaynum. "\n"
4890 my $type = $display->type;
4892 my $desc = $cust_bill_pkg->desc;
4893 $desc = substr($desc, 0, $maxlength). '...'
4894 if $format eq 'latex' && length($desc) > $maxlength;
4896 my %details_opt = ( 'format' => $format,
4897 'escape_function' => $escape_function,
4898 'format_function' => $format_function,
4899 'no_usage' => $opt{'no_usage'},
4902 if ( $cust_bill_pkg->pkgnum > 0 ) {
4904 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4907 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4909 # start/end dates for invoice formats that do nonstandard
4911 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4913 if ( (!$type || $type eq 'S')
4914 && ( $cust_bill_pkg->setup != 0
4915 || $cust_bill_pkg->setup_show_zero
4920 warn "$me _items_cust_bill_pkg adding setup\n"
4923 my $description = $desc;
4924 $description .= ' Setup'
4925 if $cust_bill_pkg->recur != 0
4926 || $discount_show_always
4927 || $cust_bill_pkg->recur_show_zero;
4930 unless ( $cust_pkg->part_pkg->hide_svc_detail
4931 || $cust_bill_pkg->hidden )
4934 push @d, map &{$escape_function}($_),
4935 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4936 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4938 if ( $multilocation ) {
4939 my $loc = $cust_pkg->location_label;
4940 $loc = substr($loc, 0, $maxlength). '...'
4941 if $format eq 'latex' && length($loc) > $maxlength;
4942 push @d, &{$escape_function}($loc);
4945 } #unless hiding service details
4947 push @d, $cust_bill_pkg->details(%details_opt)
4948 if $cust_bill_pkg->recur == 0;
4950 if ( $cust_bill_pkg->hidden ) {
4951 $s->{amount} += $cust_bill_pkg->setup;
4952 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4953 push @{ $s->{ext_description} }, @d;
4957 description => $description,
4958 #pkgpart => $part_pkg->pkgpart,
4959 pkgnum => $cust_bill_pkg->pkgnum,
4960 amount => $cust_bill_pkg->setup,
4961 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4962 unit_amount => $cust_bill_pkg->unitsetup,
4963 quantity => $cust_bill_pkg->quantity,
4964 ext_description => \@d,
4970 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4972 $cust_bill_pkg->recur != 0
4973 || $cust_bill_pkg->setup == 0
4974 || $discount_show_always
4975 || $cust_bill_pkg->recur_show_zero
4980 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4983 my $is_summary = $display->summary;
4984 my $description = ($is_summary && $type && $type eq 'U')
4985 ? "Usage charges" : $desc;
4987 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4988 " - ". time2str($date_format, $cust_bill_pkg->edate).
4990 unless $conf->exists('disable_line_item_date_ranges')
4991 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4994 my @seconds = (); # for display of usage info
4996 #at least until cust_bill_pkg has "past" ranges in addition to
4997 #the "future" sdate/edate ones... see #3032
4998 my @dates = ( $self->_date );
4999 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5000 push @dates, $prev->sdate if $prev;
5001 push @dates, undef if !$prev;
5003 unless ( $cust_pkg->part_pkg->hide_svc_detail
5004 || $cust_bill_pkg->itemdesc
5005 || $cust_bill_pkg->hidden
5006 || $is_summary && $type && $type eq 'U' )
5009 warn "$me _items_cust_bill_pkg adding service details\n"
5012 push @d, map &{$escape_function}($_),
5013 $cust_pkg->h_labels_short(@dates, 'I')
5014 #$cust_bill_pkg->edate,
5015 #$cust_bill_pkg->sdate)
5016 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5018 warn "$me _items_cust_bill_pkg done adding service details\n"
5021 if ( $multilocation ) {
5022 my $loc = $cust_pkg->location_label;
5023 $loc = substr($loc, 0, $maxlength). '...'
5024 if $format eq 'latex' && length($loc) > $maxlength;
5025 push @d, &{$escape_function}($loc);
5028 # Display of seconds_since_sqlradacct:
5029 # On the invoice, when processing @detail_items, look for a field
5030 # named 'seconds'. This will contain total seconds for each
5031 # service, in the same order as @ext_description. For services
5032 # that don't support this it will show undef.
5033 if ( $conf->exists('svc_acct-usage_seconds')
5034 and ! $cust_bill_pkg->pkgpart_override ) {
5035 foreach my $cust_svc (
5036 $cust_pkg->h_cust_svc(@dates, 'I')
5039 # eval because not having any part_export_usage exports
5040 # is a fatal error, last_bill/_date because that's how
5041 # sqlradius_hour billing does it
5043 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5045 push @seconds, $sec;
5047 } #if svc_acct-usage_seconds
5051 unless ( $is_summary ) {
5052 warn "$me _items_cust_bill_pkg adding details\n"
5055 #instead of omitting details entirely in this case (unwanted side
5056 # effects), just omit CDRs
5057 $details_opt{'no_usage'} = 1
5058 if $type && $type eq 'R';
5060 push @d, $cust_bill_pkg->details(%details_opt);
5063 warn "$me _items_cust_bill_pkg calculating amount\n"
5068 $amount = $cust_bill_pkg->recur;
5069 } elsif ($type eq 'R') {
5070 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5071 } elsif ($type eq 'U') {
5072 $amount = $cust_bill_pkg->usage;
5075 if ( !$type || $type eq 'R' ) {
5077 warn "$me _items_cust_bill_pkg adding recur\n"
5080 if ( $cust_bill_pkg->hidden ) {
5081 $r->{amount} += $amount;
5082 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5083 push @{ $r->{ext_description} }, @d;
5086 description => $description,
5087 #pkgpart => $part_pkg->pkgpart,
5088 pkgnum => $cust_bill_pkg->pkgnum,
5090 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5091 unit_amount => $cust_bill_pkg->unitrecur,
5092 quantity => $cust_bill_pkg->quantity,
5094 ext_description => \@d,
5096 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5099 } else { # $type eq 'U'
5101 warn "$me _items_cust_bill_pkg adding usage\n"
5104 if ( $cust_bill_pkg->hidden ) {
5105 $u->{amount} += $amount;
5106 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5107 push @{ $u->{ext_description} }, @d;
5110 description => $description,
5111 #pkgpart => $part_pkg->pkgpart,
5112 pkgnum => $cust_bill_pkg->pkgnum,
5114 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5115 unit_amount => $cust_bill_pkg->unitrecur,
5116 quantity => $cust_bill_pkg->quantity,
5118 ext_description => \@d,
5123 } # recurring or usage with recurring charge
5125 } else { #pkgnum tax or one-shot line item (??)
5127 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5130 if ( $cust_bill_pkg->setup != 0 ) {
5132 'description' => $desc,
5133 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5136 if ( $cust_bill_pkg->recur != 0 ) {
5138 'description' => "$desc (".
5139 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5140 time2str($date_format, $cust_bill_pkg->edate). ')',
5141 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5149 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5150 && $conf->exists('discount-show-always'));
5154 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5156 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5157 $_->{amount} =~ s/^\-0\.00$/0.00/;
5158 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5160 if $_->{amount} != 0
5161 || $discount_show_always
5162 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5163 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5167 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5174 sub _items_credits {
5175 my( $self, %opt ) = @_;
5176 my $trim_len = $opt{'trim_len'} || 60;
5180 foreach ( $self->cust_credited ) {
5182 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5184 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5185 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5186 $reason = " ($reason) " if $reason;
5189 #'description' => 'Credit ref\#'. $_->crednum.
5190 # " (". time2str("%x",$_->cust_credit->_date) .")".
5192 'description' => $self->mt('Credit applied').' '.
5193 time2str($date_format,$_->cust_credit->_date). $reason,
5194 'amount' => sprintf("%.2f",$_->amount),
5202 sub _items_payments {
5206 #get & print payments
5207 foreach ( $self->cust_bill_pay ) {
5209 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5212 'description' => $self->mt('Payment received').' '.
5213 time2str($date_format,$_->cust_pay->_date ),
5214 'amount' => sprintf("%.2f", $_->amount )
5222 =item _items_discounts_avail
5224 Returns an array of line item hashrefs representing available term discounts
5225 for this invoice. This makes the same assumptions that apply to term
5226 discounts in general: that the package is billed monthly, at a flat rate,
5227 with no usage charges. A prorated first month will be handled, as will
5228 a setup fee if the discount is allowed to apply to setup fees.
5232 sub _items_discounts_avail {
5234 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5236 my %plans = $self->discount_plans;
5238 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5242 my $plan = $plans{$months};
5244 my $term_total = sprintf('%.2f', $plan->discounted_total);
5245 my $percent = sprintf('%.0f',
5246 100 * (1 - $term_total / $plan->base_total) );
5247 my $permonth = sprintf('%.2f', $term_total / $months);
5248 my $detail = $self->mt('discount on item'). ' '.
5249 join(', ', map { "#$_" } $plan->pkgnums)
5253 description => $self->mt('Save [_1]% by paying for [_2] months',
5255 amount => $self->mt('[_1] ([_2] per month)',
5256 $term_total, $money_char.$permonth),
5257 ext_description => ($detail || ''),
5260 sort { $b <=> $a } keys %plans;
5264 =item call_details [ OPTION => VALUE ... ]
5266 Returns an array of CSV strings representing the call details for this invoice
5267 The only option available is the boolean prepend_billed_number
5272 my ($self, %opt) = @_;
5274 my $format_function = sub { shift };
5276 if ($opt{prepend_billed_number}) {
5277 $format_function = sub {
5281 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5286 my @details = map { $_->details( 'format_function' => $format_function,
5287 'escape_function' => sub{ return() },
5291 $self->cust_bill_pkg;
5292 my $header = $details[0];
5293 ( $header, grep { $_ ne $header } @details );
5303 =item process_reprint
5307 sub process_reprint {
5308 process_re_X('print', @_);
5311 =item process_reemail
5315 sub process_reemail {
5316 process_re_X('email', @_);
5324 process_re_X('fax', @_);
5332 process_re_X('ftp', @_);
5339 sub process_respool {
5340 process_re_X('spool', @_);
5343 use Storable qw(thaw);
5347 my( $method, $job ) = ( shift, shift );
5348 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5350 my $param = thaw(decode_base64(shift));
5351 warn Dumper($param) if $DEBUG;
5362 my($method, $job, %param ) = @_;
5364 warn "re_X $method for job $job with param:\n".
5365 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5368 #some false laziness w/search/cust_bill.html
5370 my $orderby = 'ORDER BY cust_bill._date';
5372 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5374 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5376 my @cust_bill = qsearch( {
5377 #'select' => "cust_bill.*",
5378 'table' => 'cust_bill',
5379 'addl_from' => $addl_from,
5381 'extra_sql' => $extra_sql,
5382 'order_by' => $orderby,
5386 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5388 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5391 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5392 foreach my $cust_bill ( @cust_bill ) {
5393 $cust_bill->$method();
5395 if ( $job ) { #progressbar foo
5397 if ( time - $min_sec > $last ) {
5398 my $error = $job->update_statustext(
5399 int( 100 * $num / scalar(@cust_bill) )
5401 die $error if $error;
5412 =head1 CLASS METHODS
5418 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5423 my ($class, $start, $end) = @_;
5425 $class->paid_sql($start, $end). ' - '.
5426 $class->credited_sql($start, $end);
5431 Returns an SQL fragment to retreive the net amount (charged minus credited).
5436 my ($class, $start, $end) = @_;
5437 'charged - '. $class->credited_sql($start, $end);
5442 Returns an SQL fragment to retreive the amount paid against this invoice.
5447 my ($class, $start, $end) = @_;
5448 $start &&= "AND cust_bill_pay._date <= $start";
5449 $end &&= "AND cust_bill_pay._date > $end";
5450 $start = '' unless defined($start);
5451 $end = '' unless defined($end);
5452 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5453 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5458 Returns an SQL fragment to retreive the amount credited against this invoice.
5463 my ($class, $start, $end) = @_;
5464 $start &&= "AND cust_credit_bill._date <= $start";
5465 $end &&= "AND cust_credit_bill._date > $end";
5466 $start = '' unless defined($start);
5467 $end = '' unless defined($end);
5468 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5469 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5474 Returns an SQL fragment to retrieve the due date of an invoice.
5475 Currently only supported on PostgreSQL.
5480 my $conf = new FS::Conf;
5484 cust_bill.invoice_terms,
5485 cust_main.invoice_terms,
5486 \''.($conf->config('invoice_default_terms') || '').'\'
5487 ), E\'Net (\\\\d+)\'
5489 ) * 86400 + cust_bill._date'
5492 =item search_sql_where HASHREF
5494 Class method which returns an SQL WHERE fragment to search for parameters
5495 specified in HASHREF. Valid parameters are
5501 List reference of start date, end date, as UNIX timestamps.
5511 List reference of charged limits (exclusive).
5515 List reference of charged limits (exclusive).
5519 flag, return open invoices only
5523 flag, return net invoices only
5527 =item newest_percust
5531 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5535 sub search_sql_where {
5536 my($class, $param) = @_;
5538 warn "$me search_sql_where called with params: \n".
5539 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5545 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5546 push @search, "cust_main.agentnum = $1";
5550 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5551 push @search, "cust_bill.custnum = $1";
5555 if ( $param->{_date} ) {
5556 my($beginning, $ending) = @{$param->{_date}};
5558 push @search, "cust_bill._date >= $beginning",
5559 "cust_bill._date < $ending";
5563 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5564 push @search, "cust_bill.invnum >= $1";
5566 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5567 push @search, "cust_bill.invnum <= $1";
5571 if ( $param->{charged} ) {
5572 my @charged = ref($param->{charged})
5573 ? @{ $param->{charged} }
5574 : ($param->{charged});
5576 push @search, map { s/^charged/cust_bill.charged/; $_; }
5580 my $owed_sql = FS::cust_bill->owed_sql;
5583 if ( $param->{owed} ) {
5584 my @owed = ref($param->{owed})
5585 ? @{ $param->{owed} }
5587 push @search, map { s/^owed/$owed_sql/; $_; }
5592 push @search, "0 != $owed_sql"
5593 if $param->{'open'};
5594 push @search, '0 != '. FS::cust_bill->net_sql
5598 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5599 if $param->{'days'};
5602 if ( $param->{'newest_percust'} ) {
5604 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5605 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5607 my @newest_where = map { my $x = $_;
5608 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5611 grep ! /^cust_main./, @search;
5612 my $newest_where = scalar(@newest_where)
5613 ? ' AND '. join(' AND ', @newest_where)
5617 push @search, "cust_bill._date = (
5618 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5619 WHERE newest_cust_bill.custnum = cust_bill.custnum
5625 #agent virtualization
5626 my $curuser = $FS::CurrentUser::CurrentUser;
5627 if ( $curuser->username eq 'fs_queue'
5628 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5630 my $newuser = qsearchs('access_user', {
5631 'username' => $username,
5635 $curuser = $newuser;
5637 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5640 push @search, $curuser->agentnums_sql;
5642 join(' AND ', @search );
5654 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5655 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base