2 use base qw( FS::cust_bill::Search FS::Template_Mixin
3 FS::cust_main_Mixin FS::Record
7 use vars qw( $DEBUG $me );
10 use Fcntl qw(:flock); #for spool_csv
12 use List::Util qw(min max sum);
17 use Storable qw( freeze thaw );
19 use FS::UID qw( datasrc );
20 use FS::Misc qw( send_fax do_print );
21 use FS::Record qw( qsearch qsearchs dbh );
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
33 use FS::cust_bill_pay;
36 use FS::cust_bill_batch;
37 use FS::cust_bill_pay_pkg;
38 use FS::cust_credit_bill_pkg;
39 use FS::discount_plan;
40 use FS::cust_bill_void;
44 use FS::Misc::Savepoint;
47 $me = '[FS::cust_bill]';
51 FS::cust_bill - Object methods for cust_bill records
57 $record = new FS::cust_bill \%hash;
58 $record = new FS::cust_bill { 'column' => 'value' };
60 $error = $record->insert;
62 $error = $new_record->replace($old_record);
64 $error = $record->delete;
66 $error = $record->check;
68 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
70 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
72 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
74 @cust_pay_objects = $cust_bill->cust_pay;
76 $tax_amount = $record->tax;
78 @lines = $cust_bill->print_text;
79 @lines = $cust_bill->print_text('time' => $time);
83 An FS::cust_bill object represents an invoice; a declaration that a customer
84 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
85 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
86 following fields are currently supported:
92 =item invnum - primary key (assigned automatically for new invoices)
94 =item custnum - customer (see L<FS::cust_main>)
96 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
97 L<Time::Local> and L<Date::Parse> for conversion functions.
99 =item charged - amount of this invoice
101 =item invoice_terms - optional terms override for this specific invoice
109 =item billing_balance - the customer's balance immediately before generating
110 this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
111 to determine the customer's balance at a specific time.
113 =item previous_balance - the customer's balance immediately after generating
114 the invoice before this one. DEPRECATED.
116 =item printed - formerly used to track the number of times an invoice had
117 been printed; no longer used.
125 =item closed - books closed flag, empty or `Y'
127 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
129 =item agent_invid - legacy invoice number
131 =item promised_date - customer promised payment date, for collection
133 =item pending - invoice is still being generated, empty or 'Y'
143 Creates a new invoice. To add the invoice to the database, see L<"insert">.
144 Invoices are normally created by calling the bill method of a customer object
145 (see L<FS::cust_main>).
149 sub table { 'cust_bill'; }
150 sub template_conf { 'invoice_'; }
152 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
153 # (except email_subject and invnum_date_pretty)
156 $self->conf->config('notice_name') || 'Invoice'
159 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 =item void [ REASON [ , REPROCESS_CDRS ] ]
212 Voids this invoice: deletes the invoice and adds a record of the voided invoice
213 to the FS::cust_bill_void table (and related tables starting from
214 FS::cust_bill_pkg_void).
220 my $reason = scalar(@_) ? shift : '';
221 my $reprocess_cdrs = scalar(@_) ? shift : '';
223 unless (ref($reason) || !$reason) {
224 $reason = FS::reason->new_or_existing(
226 'type' => 'Invoice void',
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 my $cust_bill_void = new FS::cust_bill_void ( {
243 map { $_ => $self->get($_) } $self->fields
245 $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
246 my $error = $cust_bill_void->insert;
248 $dbh->rollback if $oldAutoCommit;
252 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
253 my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
255 $dbh->rollback if $oldAutoCommit;
260 $error = $self->delete;
262 $dbh->rollback if $oldAutoCommit;
266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
274 DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
277 This is only for internal use by V<void>, which is what you should be using.
279 DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly
280 wrong. Use B<void>, that's what it is for. Really. This means you.
286 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
288 local $SIG{HUP} = 'IGNORE';
289 local $SIG{INT} = 'IGNORE';
290 local $SIG{QUIT} = 'IGNORE';
291 local $SIG{TERM} = 'IGNORE';
292 local $SIG{TSTP} = 'IGNORE';
293 local $SIG{PIPE} = 'IGNORE';
295 my $oldAutoCommit = $FS::UID::AutoCommit;
296 local $FS::UID::AutoCommit = 0;
299 foreach my $table (qw(
306 #cust_event # problematic
307 #cust_pay_batch # unnecessary
309 foreach my $linked ( $self->$table() ) {
310 my $error = $linked->delete;
312 $dbh->rollback if $oldAutoCommit;
319 my $error = $self->SUPER::delete(@_);
321 $dbh->rollback if $oldAutoCommit;
325 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
331 =item replace [ OLD_RECORD ]
333 You can, but probably shouldn't modify invoices...
335 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
336 supplied, replaces this record. If there is an error, returns the error,
337 otherwise returns false.
341 #replace can be inherited from Record.pm
343 # replace_check is now the preferred way to #implement replace data checks
344 # (so $object->replace() works without an argument)
347 my( $new, $old ) = ( shift, shift );
348 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
349 #return "Can't change _date!" unless $old->_date eq $new->_date;
350 return "Can't change _date" unless $old->_date == $new->_date;
351 return "Can't change charged" unless $old->charged == $new->charged
352 || $old->pending eq 'Y'
353 || $old->charged == 0
354 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
360 =item add_cc_surcharge
366 sub add_cc_surcharge {
367 my ($self, $pkgnum, $amount) = (shift, shift, shift);
370 my $cust_bill_pkg = new FS::cust_bill_pkg({
371 'invnum' => $self->invnum,
375 $error = $cust_bill_pkg->insert;
376 return $error if $error;
378 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
379 $self->charged($self->charged+$amount);
380 $error = $self->replace;
381 return $error if $error;
383 $self->apply_payments_and_credits;
389 Checks all fields to make sure this is a valid invoice. If there is an error,
390 returns the error, otherwise returns false. Called by the insert and replace
399 $self->ut_numbern('invnum')
400 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
401 || $self->ut_numbern('_date')
402 || $self->ut_money('charged')
403 || $self->ut_numbern('printed')
404 || $self->ut_enum('closed', [ '', 'Y' ])
405 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
406 || $self->ut_numbern('agent_invid') #varchar?
407 || $self->ut_flag('pending')
409 return $error if $error;
411 $self->_date(time) unless $self->_date;
413 $self->printed(0) if $self->printed eq '';
420 Returns the displayed invoice number for this invoice: agent_invid if
421 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
427 if ( $self->agent_invid
428 && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
429 return $self->agent_invid;
431 return $self->invnum;
437 Returns the customer's last invoice before this one.
443 if ( !$self->get('previous_bill') ) {
444 $self->set('previous_bill', qsearchs({
445 'table' => 'cust_bill',
446 'hashref' => { 'custnum' => $self->custnum,
447 '_date' => { op=>'<', value=>$self->_date } },
448 'order_by' => 'ORDER BY _date DESC LIMIT 1',
451 $self->get('previous_bill');
456 Returns the customer's invoice that follows this one
462 if (!$self->get('following_bill')) {
463 $self->set('following_bill', qsearchs({
464 table => 'cust_bill',
466 custnum => $self->custnum,
467 invnum => { op => '>', value => $self->invnum },
469 order_by => 'ORDER BY invnum ASC LIMIT 1',
472 $self->get('following_bill');
477 Returns a list consisting of the total previous balance for this customer,
478 followed by the previous outstanding invoices (as FS::cust_bill objects also).
484 # simple memoize; we use this a lot
485 if (!$self->get('previous')) {
487 my @cust_bill = sort { $a->_date <=> $b->_date }
488 grep { $_->owed != 0 }
489 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
490 #'_date' => { op=>'<', value=>$self->_date },
491 'invnum' => { op=>'<', value=>$self->invnum },
494 foreach ( @cust_bill ) { $total += $_->owed; }
495 $self->set('previous', [$total, @cust_bill]);
497 return @{ $self->get('previous') };
500 =item enable_previous
502 Whether to show the 'Previous Charges' section when printing this invoice.
503 The negation of the 'disable_previous_balance' config setting.
507 sub enable_previous {
509 my $agentnum = $self->cust_main->agentnum;
510 !$self->conf->exists('disable_previous_balance', $agentnum);
515 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
523 'select' => 'cust_bill_pkg.*, pkg_category.categoryname',
524 'table' => 'cust_bill_pkg',
525 'addl_from' => ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
526 ' LEFT JOIN part_pkg USING ( pkgpart ) '.
527 ' LEFT JOIN pkg_class USING ( classnum ) '.
528 ' LEFT JOIN pkg_category USING ( categorynum ) ',
529 'hashref' => { 'invnum' => $self->invnum },
530 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
531 # the AUTLOADED FK search. or should
532 # that default to ORDER by the pkey?
537 =item cust_bill_pkg_pkgnum PKGNUM
539 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
544 sub cust_bill_pkg_pkgnum {
545 my( $self, $pkgnum ) = @_;
547 { 'table' => 'cust_bill_pkg',
548 'hashref' => { 'invnum' => $self->invnum,
551 'order_by' => 'ORDER BY billpkgnum',
558 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
565 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
566 $self->cust_bill_pkg;
568 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
573 Returns true if any of the packages (or their definitions) corresponding to the
574 line items for this invoice have the no_auto flag set.
580 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
583 =item open_cust_bill_pkg
585 Returns the open line items for this invoice.
587 Note that cust_bill_pkg with both setup and recur fees are returned as two
588 separate line items, each with only one fee.
592 # modeled after cust_main::open_cust_bill
593 sub open_cust_bill_pkg {
596 # grep { $_->owed > 0 } $self->cust_bill_pkg
598 my %other = ( 'recur' => 'setup',
599 'setup' => 'recur', );
601 foreach my $field ( qw( recur setup )) {
602 push @open, map { $_->set( $other{$field}, 0 ); $_; }
603 grep { $_->owed($field) > 0 }
604 $self->cust_bill_pkg;
612 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
616 #false laziness w/cust_pkg.pm
620 'table' => 'cust_event',
621 'addl_from' => 'JOIN part_event USING ( eventpart )',
622 'hashref' => { 'tablenum' => $self->invnum },
623 'extra_sql' => " AND eventtable = 'cust_bill' ",
629 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
633 #false laziness w/cust_pkg.pm
637 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
638 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
639 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
640 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
641 $sth->fetchrow_arrayref->[0];
646 Returns the customer (see L<FS::cust_main>) for this invoice.
650 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
652 Returns a list: an empty list on success or a list of errors.
659 grep { $_->suspend(@_) }
660 grep {! $_->getfield('cancel') }
665 =item cust_suspend_if_balance_over AMOUNT
667 Suspends the customer associated with this invoice if the total amount owed on
668 this invoice and all older invoices is greater than the specified amount.
670 Returns a list: an empty list on success or a list of errors.
674 sub cust_suspend_if_balance_over {
675 my( $self, $amount ) = ( shift, shift );
676 my $cust_main = $self->cust_main;
677 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
680 $cust_main->suspend(@_);
686 Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
691 my( $self, %opt ) = @_;
693 warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
694 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
697 return ( 'Access denied' )
698 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
700 my @pkgs = $self->cust_pkg;
702 if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
704 my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
705 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
710 map { $_->cancel(%opt) }
711 grep { ! $_->getfield('cancel') }
717 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
723 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
724 sort { $a->_date <=> $b->_date }
725 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
730 =item cust_credit_bill
732 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
738 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
739 sort { $a->_date <=> $b->_date }
740 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
744 sub cust_credit_bill {
745 shift->cust_credited(@_);
748 #=item cust_bill_pay_pkgnum PKGNUM
750 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
751 #with matching pkgnum.
755 #sub cust_bill_pay_pkgnum {
756 # my( $self, $pkgnum ) = @_;
757 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
758 # sort { $a->_date <=> $b->_date }
759 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
760 # 'pkgnum' => $pkgnum,
765 =item cust_bill_pay_pkg PKGNUM
767 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
768 applied against the matching pkgnum.
772 sub cust_bill_pay_pkg {
773 my( $self, $pkgnum ) = @_;
776 'select' => 'cust_bill_pay_pkg.*',
777 'table' => 'cust_bill_pay_pkg',
778 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
779 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
780 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
781 " AND cust_bill_pkg.pkgnum = $pkgnum",
786 #=item cust_credited_pkgnum PKGNUM
788 #=item cust_credit_bill_pkgnum PKGNUM
790 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
791 #with matching pkgnum.
795 #sub cust_credited_pkgnum {
796 # my( $self, $pkgnum ) = @_;
797 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
798 # sort { $a->_date <=> $b->_date }
799 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
800 # 'pkgnum' => $pkgnum,
805 #sub cust_credit_bill_pkgnum {
806 # shift->cust_credited_pkgnum(@_);
809 =item cust_credit_bill_pkg PKGNUM
811 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
812 applied against the matching pkgnum.
816 sub cust_credit_bill_pkg {
817 my( $self, $pkgnum ) = @_;
820 'select' => 'cust_credit_bill_pkg.*',
821 'table' => 'cust_credit_bill_pkg',
822 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
823 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
824 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
825 " AND cust_bill_pkg.pkgnum = $pkgnum",
830 =item cust_bill_batch
832 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
836 sub cust_bill_batch {
838 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
843 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
844 hash keyed by term length.
850 FS::discount_plan->all($self);
855 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
862 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
864 foreach (@taxlines) { $total += $_->setup; }
870 Returns the amount owed (still outstanding) on this invoice, which is charged
871 minus all payment applications (see L<FS::cust_bill_pay>) and credit
872 applications (see L<FS::cust_credit_bill>).
878 my $balance = $self->charged;
879 $balance -= $_->amount foreach ( $self->cust_bill_pay );
880 $balance -= $_->amount foreach ( $self->cust_credited );
881 $balance = sprintf( "%.2f", $balance);
882 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
886 =item owed_on_invoice
888 Returns the amount to be displayed as the "Balance Due" on this
889 invoice. Amount returned depends on conf flags for invoicing
891 See L<FS::cust_bill::owed> for the true amount currently owed
895 sub owed_on_invoice {
898 #return $self->owed()
899 # unless $self->conf->exists('previous_balance-payments_since')
901 # Add charges from this invoice
902 my $owed = $self->charged();
904 # Add carried balances from previous invoices
905 # If previous items aren't to be displayed on the invoice,
906 # _items_previous() is aware of this and responds appropriately.
907 $owed += $_->{amount} for $self->_items_previous();
909 # Subtract payments and credits displayed on this invoice
910 $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
916 my( $self, $pkgnum ) = @_;
918 #my $balance = $self->charged;
920 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
922 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
923 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
925 $balance = sprintf( "%.2f", $balance);
926 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
932 Returns true if this invoice should be hidden. See the
933 selfservice-hide_invoices-taxclass configuraiton setting.
939 my $conf = $self->conf;
940 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
942 my @cust_bill_pkg = $self->cust_bill_pkg;
943 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
944 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
947 =item apply_payments_and_credits [ OPTION => VALUE ... ]
949 Applies unapplied payments and credits to this invoice.
950 Payments with the no_auto_apply flag set will not be applied.
952 A hash of optional arguments may be passed. Currently "manual" is supported.
953 If true, a payment receipt is sent instead of a statement when
954 'payment_receipt_email' configuration option is set.
956 If there is an error, returns the error, otherwise returns false.
960 sub apply_payments_and_credits {
961 my( $self, %options ) = @_;
962 my $conf = $self->conf;
964 local $SIG{HUP} = 'IGNORE';
965 local $SIG{INT} = 'IGNORE';
966 local $SIG{QUIT} = 'IGNORE';
967 local $SIG{TERM} = 'IGNORE';
968 local $SIG{TSTP} = 'IGNORE';
969 local $SIG{PIPE} = 'IGNORE';
971 my $oldAutoCommit = $FS::UID::AutoCommit;
972 local $FS::UID::AutoCommit = 0;
975 my $savepoint_label = 'cust_bill__apply_payments_and_credits';
976 savepoint_create( $savepoint_label );
978 $self->select_for_update; #mutex
980 my @payments = grep { $_->unapplied > 0 }
981 grep { !$_->no_auto_apply }
982 $self->cust_main->cust_pay;
983 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
985 if ( $conf->exists('pkg-balances') ) {
986 # limit @payments & @credits to those w/ a pkgnum grepped from $self
987 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
988 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
989 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
992 while ( $self->owed > 0 and ( @payments || @credits ) ) {
995 if ( @payments && @credits ) {
997 #decide which goes first by weight of top (unapplied) line item
999 my @open_lineitems = $self->open_cust_bill_pkg;
1001 my $max_pay_weight =
1002 max( map { $_->part_pkg->pay_weight || 0 }
1004 map { $_->cust_pkg }
1007 my $max_credit_weight =
1008 max( map { $_->part_pkg->credit_weight || 0 }
1010 map { $_->cust_pkg }
1014 #if both are the same... payments first? it has to be something
1015 if ( $max_pay_weight >= $max_credit_weight ) {
1021 } elsif ( @payments ) {
1023 } elsif ( @credits ) {
1026 die "guru meditation #12 and 35";
1030 if ( $app eq 'pay' ) {
1032 my $payment = shift @payments;
1033 $unapp_amount = $payment->unapplied;
1034 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
1035 $app->pkgnum( $payment->pkgnum )
1036 if $conf->exists('pkg-balances') && $payment->pkgnum;
1038 } elsif ( $app eq 'credit' ) {
1040 my $credit = shift @credits;
1041 $unapp_amount = $credit->credited;
1042 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
1043 $app->pkgnum( $credit->pkgnum )
1044 if $conf->exists('pkg-balances') && $credit->pkgnum;
1047 die "guru meditation #12 and 35";
1051 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
1052 warn "owed_pkgnum ". $app->pkgnum;
1053 $owed = $self->owed_pkgnum($app->pkgnum);
1055 $owed = $self->owed;
1057 next unless $owed > 0;
1059 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
1060 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
1062 $app->invnum( $self->invnum );
1064 my $error = $app->insert(%options);
1066 savepoint_rollback_and_release( $savepoint_label );
1067 $dbh->rollback if $oldAutoCommit;
1068 return "Error inserting ". $app->table. " record: $error";
1070 die $error if $error;
1074 savepoint_release( $savepoint_label );
1075 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1082 Sends this invoice to the destinations configured for this customer: sends
1083 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1085 Options can be passed as a hashref. Positional parameters are no longer
1088 I<template>: a suffix for alternate invoices
1090 I<agentnum>: obsolete, now does nothing.
1092 I<from> overrides the default email invoice From: address.
1094 I<amount>: obsolete, does nothing
1096 I<notice_name> overrides "Invoice" as the name of the sent document
1097 (templates from 10/2009 or newer required).
1099 I<lpr> overrides the system 'lpr' option as the command to print a document
1100 from standard input.
1106 my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1107 my $conf = $self->conf;
1109 my $cust_main = $self->cust_main;
1111 my @invoicing_list = $cust_main->invoicing_list;
1114 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1115 && ! $cust_main->invoice_noemail;
1118 if grep { $_ eq 'POST' } @invoicing_list; #postal
1120 #this has never been used post-$ORIGINAL_ISP afaik
1121 $self->fax_invoice($opt)
1122 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1130 my $opt = shift || {};
1131 if ($opt and !ref($opt)) {
1132 die ref($self). '->email called with positional parameters';
1135 my $conf = $self->conf;
1137 my $from = delete $opt->{from};
1139 # this is where we set the From: address
1140 $from ||= $self->_agent_invoice_from || #XXX should go away
1141 $conf->invoice_from_full( $self->cust_main->agentnum );
1143 my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1145 if ( ! @invoicing_list ) { #no recipients
1146 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1147 die 'No recipients for customer #'. $self->custnum;
1149 #default: better to notify this person than silence
1150 @invoicing_list = ($from);
1154 $self->SUPER::email( {
1156 'to' => \@invoicing_list,
1162 #this stays here for now because its explicitly used as
1163 # FS::cust_bill::queueable_email
1164 sub queueable_email {
1167 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1168 or die "invalid invoice number: " . $opt{invnum};
1170 $self->set('mode', $opt{mode})
1173 my %args = map {$_ => $opt{$_}}
1175 qw( from notice_name no_coupon template );
1177 my $error = $self->email( \%args );
1178 die $error if $error;
1184 my $conf = $self->conf;
1186 #my $template = scalar(@_) ? shift : '';
1189 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1192 my $cust_main = $self->cust_main;
1193 my $name = $cust_main->name;
1194 my $name_short = $cust_main->name_short;
1195 my $invoice_number = $self->invnum;
1196 my $invoice_date = $self->_date_pretty;
1198 eval qq("$subject");
1203 'Invoice-'. $self->invnum. '.pdf';
1206 =item lpr_data HASHREF
1208 Returns the postscript or plaintext for this invoice as an arrayref.
1210 Options must be passed as a hashref. Positional parameters are no longer
1213 I<template>, if specified, is the name of a suffix for alternate invoices.
1215 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1221 my $conf = $self->conf;
1222 my $opt = shift || {};
1223 if ($opt and !ref($opt)) {
1224 # nobody does this anyway
1225 die "FS::cust_bill::lpr_data called with positional parameters";
1228 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1229 [ $self->$method( $opt ) ];
1234 Prints this invoice.
1236 Options must be passed as a hashref.
1238 I<template>, if specified, is the name of a suffix for alternate invoices.
1240 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1246 return if $self->hide;
1247 my $conf = $self->conf;
1248 my $opt = shift || {};
1249 if ($opt and !ref($opt)) {
1250 die "FS::cust_bill::print called with positional parameters";
1253 my $lpr = delete $opt->{lpr};
1254 if($conf->exists('invoice_print_pdf')) {
1255 # Add the invoice to the current batch.
1256 $self->batch_invoice($opt);
1260 $self->lpr_data($opt),
1261 'agentnum' => $self->cust_main->agentnum,
1267 =item fax_invoice HASHREF
1271 Options must be passed as a hashref.
1273 I<template>, if specified, is the name of a suffix for alternate invoices.
1275 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1281 return if $self->hide;
1282 my $conf = $self->conf;
1283 my $opt = shift || {};
1284 if ($opt and !ref($opt)) {
1285 die "FS::cust_bill::fax_invoice called with positional parameters";
1288 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1289 unless $conf->exists('invoice_latex');
1291 my $dialstring = $self->cust_main->getfield('fax');
1294 my $error = send_fax( 'docdata' => $self->lpr_data($opt),
1295 'dialstring' => $dialstring,
1297 die $error if $error;
1301 =item batch_invoice [ HASHREF ]
1303 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1304 isn't an open batch, one will be created.
1306 HASHREF may contain any options to be passed to C<print_pdf>.
1311 my ($self, $opt) = @_;
1312 my $bill_batch = $self->get_open_bill_batch;
1313 my $cust_bill_batch = FS::cust_bill_batch->new({
1314 batchnum => $bill_batch->batchnum,
1315 invnum => $self->invnum,
1317 if ( $self->mode ) {
1318 $opt->{mode} ||= $self->mode;
1319 $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
1321 return $cust_bill_batch->insert($opt);
1324 =item get_open_batch
1326 Returns the currently open batch as an FS::bill_batch object, creating a new
1327 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1332 sub get_open_bill_batch {
1334 my $conf = $self->conf;
1335 my $hashref = { status => 'O' };
1336 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1337 ? $self->cust_main->agentnum
1339 my $batch = qsearchs('bill_batch', $hashref);
1340 return $batch if $batch;
1341 $batch = FS::bill_batch->new($hashref);
1342 my $error = $batch->insert;
1343 die $error if $error;
1347 =item ftp_invoice [ TEMPLATENAME ]
1349 Sends this invoice data via FTP.
1351 TEMPLATENAME is unused?
1357 my $conf = $self->conf;
1358 my $template = scalar(@_) ? shift : '';
1361 'protocol' => 'ftp',
1362 'server' => $conf->config('cust_bill-ftpserver'),
1363 'username' => $conf->config('cust_bill-ftpusername'),
1364 'password' => $conf->config('cust_bill-ftppassword'),
1365 'dir' => $conf->config('cust_bill-ftpdir'),
1366 'format' => $conf->config('cust_bill-ftpformat'),
1370 =item spool_invoice [ TEMPLATENAME ]
1372 Spools this invoice data (see L<FS::spool_csv>)
1374 TEMPLATENAME is unused?
1380 my $conf = $self->conf;
1381 my $template = scalar(@_) ? shift : '';
1384 'format' => $conf->config('cust_bill-spoolformat'),
1385 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1389 =item send_csv OPTION => VALUE, ...
1391 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1395 protocol - currently only "ftp"
1401 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1402 and YYMMDDHHMMSS is a timestamp.
1404 See L</print_csv> for a description of the output format.
1409 my($self, %opt) = @_;
1411 if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
1412 warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
1418 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1419 mkdir $spooldir, 0700 unless -d $spooldir;
1421 # don't localize dates here, they're a defined format
1422 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1423 my $file = "$spooldir/$tracctnum.csv";
1425 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1427 open(CSV, ">$file") or die "can't open $file: $!";
1435 if ( $opt{protocol} eq 'ftp' ) {
1436 eval "use Net::FTP;";
1438 $net = Net::FTP->new($opt{server}) or die @$;
1440 die "unknown protocol: $opt{protocol}";
1443 $net->login( $opt{username}, $opt{password} )
1444 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1446 $net->binary or die "can't set binary mode";
1448 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1450 $net->put($file) or die "can't put $file: $!";
1460 Spools CSV invoice data.
1466 =item format - any of FS::Misc::::Invoicing::spool_formats
1468 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1469 customer has the corresponding invoice destinations set (see
1470 L<FS::cust_main_invoice>).
1472 =item agent_spools - if set to a true value, will spool to per-agent files
1473 rather than a single global file
1475 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1476 append to that spool. L<FS::Cron::upload> will then send the spool file to
1479 =item balanceover - if set, only spools the invoice if the total amount owed on
1480 this invoice and all older invoices is greater than the specified amount.
1482 =item time - the "current time". Controls the printing of past due messages
1490 my($self, %opt) = @_;
1492 if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
1493 warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
1497 my $time = $opt{'time'} || time;
1498 my $cust_main = $self->cust_main;
1500 if ( $opt{'dest'} ) {
1501 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1502 $cust_main->invoicing_list;
1503 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1504 || ! keys %invoicing_list;
1507 if ( $opt{'balanceover'} ) {
1509 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1512 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1513 mkdir $spooldir, 0700 unless -d $spooldir;
1515 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1518 if ( $opt{'agent_spools'} ) {
1519 $file = 'agentnum'.$cust_main->agentnum;
1524 if ( $opt{'upload_targetnum'} ) {
1525 $spooldir .= '/target'.$opt{'upload_targetnum'};
1526 mkdir $spooldir, 0700 unless -d $spooldir;
1527 } # otherwise it just goes into export.xxx/cust_bill
1529 if ( lc($opt{'format'}) eq 'billco' ) {
1533 $file = "$spooldir/$file.csv";
1535 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1537 open(CSV, ">>$file") or die "can't open $file: $!";
1538 flock(CSV, LOCK_EX);
1543 if ( lc($opt{'format'}) eq 'billco' ) {
1545 flock(CSV, LOCK_UN);
1548 $file =~ s/-header.csv$/-detail.csv/;
1550 open(CSV,">>$file") or die "can't open $file: $!";
1551 flock(CSV, LOCK_EX);
1555 print CSV $detail if defined($detail);
1557 flock(CSV, LOCK_UN);
1564 =item print_csv OPTION => VALUE, ...
1566 Returns CSV data for this invoice.
1570 format - 'default', 'billco', 'oneline', 'bridgestone'
1572 Returns a list consisting of two scalars. The first is a single line of CSV
1573 header information for this invoice. The second is one or more lines of CSV
1574 detail information for this invoice.
1576 If I<format> is not specified or "default", the fields of the CSV file are as
1579 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1580 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1584 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1586 B<record_type> is C<cust_bill> for the initial header line only. The
1587 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1588 fields are filled in.
1590 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1591 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1594 =item invnum - invoice number
1596 =item custnum - customer number
1598 =item _date - invoice date
1600 =item charged - total invoice amount
1602 =item first - customer first name
1604 =item last - customer first name
1606 =item company - company name
1608 =item address1 - address line 1
1610 =item address2 - address line 1
1620 =item pkg - line item description
1622 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1624 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1626 =item sdate - start date for recurring fee
1628 =item edate - end date for recurring fee
1632 If I<format> is "billco", the fields of the header CSV file are as follows:
1634 +-------------------------------------------------------------------+
1635 | FORMAT HEADER FILE |
1636 |-------------------------------------------------------------------|
1637 | Field | Description | Name | Type | Width |
1638 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1639 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1640 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1641 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1642 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1643 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1644 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1645 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1646 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1647 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1648 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1649 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1650 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1651 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1652 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1653 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1654 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1655 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1656 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1657 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1658 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1659 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1660 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1661 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1662 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1663 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1664 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1665 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1666 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1667 +-------+-------------------------------+------------+------+-------+
1669 If I<format> is "billco", the fields of the detail CSV file are as follows:
1671 FORMAT FOR DETAIL FILE
1673 Field | Description | Name | Type | Width
1674 1 | N/A-Leave Empty | RC | CHAR | 2
1675 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1676 3 | Account Number | TRACCTNUM | CHAR | 15
1677 4 | Invoice Number | TRINVOICE | CHAR | 15
1678 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1679 6 | Transaction Detail | DETAILS | CHAR | 100
1680 7 | Amount | AMT | NUM* | 9
1681 8 | Line Format Control** | LNCTRL | CHAR | 2
1682 9 | Grouping Code | GROUP | CHAR | 2
1683 10 | User Defined | ACCT CODE | CHAR | 15
1685 If format is 'oneline', there is no detail file. Each invoice has a
1686 header line only, with the fields:
1688 Agent number, agent name, customer number, first name, last name, address
1689 line 1, address line 2, city, state, zip, invoice date, invoice number,
1690 amount charged, amount due, previous balance, due date.
1692 and then, for each line item, three columns containing the package number,
1693 description, and amount.
1695 If format is 'bridgestone', there is no detail file. Each invoice has a
1696 header line with the following fields in a fixed-width format:
1698 Customer number (in display format), date, name (first last), company,
1699 address 1, address 2, city, state, zip.
1701 This is a mailing list format, and has no per-invoice fields. To avoid
1702 sending redundant notices, the spooling event should have a "once" or
1703 "once_percust_every" condition.
1708 my($self, %opt) = @_;
1710 eval "use Text::CSV_XS";
1713 my $cust_main = $self->cust_main;
1715 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1716 my $format = lc($opt{'format'});
1718 my $time = $opt{'time'} || time;
1720 $self->set('_template', $opt{template})
1721 if exists $opt{template};
1723 my $tracctnum = ''; #leaking out from billco-specific sections :/
1724 if ( $format eq 'billco' ) {
1727 $self->conf->config('billco-account_num', $cust_main->agentnum);
1729 $tracctnum = $account_num eq 'display_custnum'
1730 ? $cust_main->display_custnum
1731 : $opt{'tracctnum'};
1734 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1736 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1738 my( $previous_balance, @unused ) = $self->previous; #previous balance
1740 my $pmt_cr_applied = 0;
1741 $pmt_cr_applied += $_->{'amount'}
1742 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1744 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1747 '', # 1 | N/A-Leave Empty CHAR 2
1748 '', # 2 | N/A-Leave Empty CHAR 15
1749 $tracctnum, # 3 | Transaction Account No CHAR 15
1750 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1751 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1752 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1753 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1754 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1755 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1756 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1757 '', # 10 | Ancillary Billing Information CHAR 30
1758 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1759 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1762 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1765 $duedate, # 14 | Bill Due Date CHAR 10
1767 $previous_balance, # 15 | Previous Balance NUM* 9
1768 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1769 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1770 $totaldue, # 18 | Total Amt Due NUM* 9
1771 $totaldue, # 19 | Total Amt Due NUM* 9
1772 '', # 20 | 30 Day Aging NUM* 9
1773 '', # 21 | 60 Day Aging NUM* 9
1774 '', # 22 | 90 Day Aging NUM* 9
1775 'N', # 23 | Y/N CHAR 1
1776 '', # 24 | Remittance automation CHAR 100
1777 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1778 $self->custnum, # 26 | Customer Reference Number CHAR 15
1779 '0', # 27 | Federal Tax*** NUM* 9
1780 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1781 '0', # 29 | Other Taxes & Fees*** NUM* 9
1784 } elsif ( $format eq 'oneline' ) { #name
1786 my ($previous_balance) = $self->previous;
1787 $previous_balance = sprintf('%.2f', $previous_balance);
1788 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1794 $self->_items_pkg, #_items_nontax? no sections or anything
1799 $cust_main->agentnum,
1800 $cust_main->agent->agent,
1804 $cust_main->company,
1805 $cust_main->address1,
1806 $cust_main->address2,
1812 time2str("%x", $self->_date),
1817 $self->due_date2str("%x"),
1822 } elsif ( $format eq 'bridgestone' ) {
1824 # bypass the CSV stuff and just return this
1825 my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1826 my $zip = $cust_main->zip;
1828 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1832 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1834 $cust_main->display_custnum,
1836 uc(substr($cust_main->contact_firstlast,0,30)),
1837 uc(substr($cust_main->company ,0,30)),
1838 uc(substr($cust_main->address1 ,0,30)),
1839 uc(substr($cust_main->address2 ,0,30)),
1840 uc(substr($cust_main->city ,0,20)),
1841 uc($cust_main->state),
1847 } elsif ( $format eq 'ics' ) {
1849 my $bill = $cust_main->bill_location;
1850 my $zip = $bill->zip;
1854 if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1859 # minor false laziness with print_generic
1860 my ($previous_balance) = $self->previous;
1861 my $balance_due = $self->owed + $previous_balance;
1862 my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1863 my $credit_total = sum(0, map { $_->{'amount'} } $self->_items_credits);
1866 if ( $self->due_date and $time >= $self->due_date ) {
1867 $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1871 my $header = sprintf(
1872 '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1873 $cust_main->display_custnum, #BID
1874 uc($cust_main->first), #FNAME
1875 uc($cust_main->last), #LNAME
1876 '00', #BATCH, should this ever be anything else?
1877 uc($cust_main->company), #COMP
1878 uc($bill->address1), #STREET1
1879 uc($bill->address2), #STREET2
1880 uc($bill->city), #CITY
1881 uc($bill->state), #STATE
1884 time2str('%Y%m%d', $self->_date), #BILL_DATE
1885 $self->due_date2str('%Y%m%d'), #DUE_DATE,
1886 ( map {sprintf('%0.2f', $_)}
1887 $balance_due, #AMNT_DUE
1888 $previous_balance, #PREV_BAL
1889 $payment_total, #PYMT_RCVD
1890 $credit_total, #CREDITS
1891 $previous_balance, #BEG_BAL--is this correct?
1892 $self->charged, #NEW_CHRG
1895 $past_due, #PAST_MSG
1899 my %svc_class = ('' => ''); # maybe cache this more persistently?
1901 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1903 my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1904 my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1908 my @dates = ( $self->_date, undef );
1909 if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1910 $dates[1] = $prev->sdate; #questionable
1913 # generate an 01 detail for each service
1914 my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1915 foreach my $cust_svc ( @svcs ) {
1916 $show_pkgnum = ''; # hide it if we're showing svcnums
1918 my $svcpart = $cust_svc->svcpart;
1919 if (!exists($svc_class{$svcpart})) {
1920 my $classnum = $cust_svc->part_svc->classnum;
1921 my $part_svc_class = FS::part_svc_class->by_key($classnum)
1923 $svc_class{$svcpart} = $part_svc_class ?
1924 $part_svc_class->classname :
1928 my @h_label = $cust_svc->label(@dates, 'I');
1929 push @details, sprintf('01%-9s%-20s%-47s',
1931 $svc_class{$svcpart},
1934 } #foreach $cust_svc
1937 my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1938 if ($cust_bill_pkg->recur > 0) {
1939 $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1940 time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1942 push @details, sprintf('02%-6s%-60s%-10s',
1945 sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1947 } #foreach $cust_bill_pkg
1949 # Tag this row so that we know whether this is one page (1), two pages
1950 # (2), # or "big" (B). The tag will be stripped off before uploading.
1951 if ( scalar(@details) < 12 ) {
1953 } elsif ( scalar(@details) < 58 ) {
1959 return join('', $header, @details, "\n");
1967 time2str("%x", $self->_date),
1968 sprintf("%.2f", $self->charged),
1969 ( map { $cust_main->getfield($_) }
1970 qw( first last company address1 address2 city state zip country ) ),
1972 ) or die "can't create csv";
1975 my $header = $csv->string. "\n";
1978 if ( lc($opt{'format'}) eq 'billco' ) {
1981 my %items_opt = ( format => 'template',
1982 escape_function => sub { shift } );
1983 # I don't know what characters billco actually tolerates in spool entries.
1984 # Text::CSV will take care of delimiters, though.
1986 my @items = ( $self->_items_pkg(%items_opt),
1987 $self->_items_fee(%items_opt) );
1988 foreach my $item (@items) {
1990 my $description = $item->{'description'};
1991 if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1992 $description .= ': ' . $item->{ext_description}[0];
1996 '', # 1 | N/A-Leave Empty CHAR 2
1997 '', # 2 | N/A-Leave Empty CHAR 15
1998 $tracctnum, # 3 | Account Number CHAR 15
1999 $self->invnum, # 4 | Invoice Number CHAR 15
2000 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2001 $description, # 6 | Transaction Detail CHAR 100
2002 $item->{'amount'}, # 7 | Amount NUM* 9
2003 '', # 8 | Line Format Control** CHAR 2
2004 '', # 9 | Grouping Code CHAR 2
2005 '', # 10 | User Defined CHAR 15
2008 $detail .= $csv->string. "\n";
2012 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2018 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2020 my($pkg, $setup, $recur, $sdate, $edate);
2021 if ( $cust_bill_pkg->pkgnum ) {
2023 ($pkg, $setup, $recur, $sdate, $edate) = (
2024 $cust_bill_pkg->part_pkg->pkg,
2025 ( $cust_bill_pkg->setup != 0
2026 ? sprintf("%.2f", $cust_bill_pkg->setup )
2028 ( $cust_bill_pkg->recur != 0
2029 ? sprintf("%.2f", $cust_bill_pkg->recur )
2031 ( $cust_bill_pkg->sdate
2032 ? time2str("%x", $cust_bill_pkg->sdate)
2034 ($cust_bill_pkg->edate
2035 ? time2str("%x", $cust_bill_pkg->edate)
2039 } else { #pkgnum tax
2040 next unless $cust_bill_pkg->setup != 0;
2041 $pkg = $cust_bill_pkg->desc;
2042 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2043 ( $sdate, $edate ) = ( '', '' );
2049 ( map { '' } (1..11) ),
2050 ($pkg, $setup, $recur, $sdate, $edate)
2051 ) or die "can't create csv";
2053 $detail .= $csv->string. "\n";
2059 ( $header, $detail );
2064 croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
2069 Attempts to pay this invoice with a credit card payment via a
2070 Business::OnlinePayment realtime gateway. See
2071 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2072 for supported processors.
2078 $self->realtime_bop( 'CC', @_ );
2083 Attempts to pay this invoice with an electronic check (ACH) payment via a
2084 Business::OnlinePayment realtime gateway. See
2085 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2086 for supported processors.
2092 $self->realtime_bop( 'ECHECK', @_ );
2097 Attempts to pay this invoice with phone bill (LEC) payment via a
2098 Business::OnlinePayment realtime gateway. See
2099 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2100 for supported processors.
2106 $self->realtime_bop( 'LEC', @_ );
2110 my( $self, $method ) = (shift,shift);
2111 my $conf = $self->conf;
2114 my $cust_main = $self->cust_main;
2115 my $balance = $cust_main->balance;
2116 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2117 $amount = sprintf("%.2f", $amount);
2118 return "not run (balance $balance)" unless $amount > 0;
2120 my $description = 'Internet Services';
2121 if ( $conf->exists('business-onlinepayment-description') ) {
2122 my $dtempl = $conf->config('business-onlinepayment-description');
2124 my $agent_obj = $cust_main->agent
2125 or die "can't retreive agent for $cust_main (agentnum ".
2126 $cust_main->agentnum. ")";
2127 my $agent = $agent_obj->agent;
2128 my $pkgs = join(', ',
2129 map { $_->part_pkg->pkg }
2130 grep { $_->pkgnum } $self->cust_bill_pkg
2132 $description = eval qq("$dtempl");
2135 $cust_main->realtime_bop($method, $amount,
2136 'description' => $description,
2137 'invnum' => $self->invnum,
2138 #this didn't do what we want, it just calls apply_payments_and_credits
2140 'apply_to_invoice' => 1,
2143 #this changes application behavior: auto payments
2144 #triggered against a specific invoice are now applied
2145 #to that invoice instead of oldest open.
2151 =item batch_card OPTION => VALUE...
2153 Adds a payment for this invoice to the pending credit card batch (see
2154 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2155 runs the payment using a realtime gateway.
2160 my ($self, %options) = @_;
2161 my $cust_main = $self->cust_main;
2163 $options{invnum} = $self->invnum;
2165 $cust_main->batch_card(%options);
2168 sub _agent_template {
2170 $self->cust_main->agent_template;
2173 sub _agent_invoice_from {
2175 $self->cust_main->agent_invoice_from;
2178 =item invoice_barcode DIR_OR_FALSE
2180 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2181 it is taken as the temp directory where the PNG file will be generated and the
2182 PNG file name is returned. Otherwise, the PNG image itself is returned.
2186 sub invoice_barcode {
2187 my ($self, $dir) = (shift,shift);
2189 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2190 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2191 my $gd = $gdbar->plot(Height => 30);
2194 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2198 ) or die "can't open temp file: $!\n";
2199 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2200 my $png_file = $bh->filename;
2207 =item invnum_date_pretty
2209 Returns a string with the invoice number and date, for example:
2210 "Invoice #54 (3/20/2008)".
2212 Intended for back-end context, with regard to translation and date formatting.
2216 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2217 # for backend use (and also does the wrong thing, localizing for end customer
2218 # instead of backoffice configured date format)
2219 sub invnum_date_pretty {
2221 #$self->mt('Invoice #').
2222 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2223 $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2226 #sub _items_extra_usage_sections {
2228 # my $escape = shift;
2230 # my %sections = ();
2232 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
2233 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2235 # next unless $cust_bill_pkg->pkgnum > 0;
2237 # foreach my $section ( keys %usage_class ) {
2239 # my $usage = $cust_bill_pkg->usage($section);
2241 # next unless $usage && $usage > 0;
2243 # $sections{$section} ||= 0;
2244 # $sections{$section} += $usage;
2250 # map { { 'description' => &{$escape}($_),
2251 # 'subtotal' => $sections{$_},
2252 # 'summarized' => '',
2253 # 'tax_section' => '',
2256 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2260 sub _items_extra_usage_sections {
2262 my $conf = $self->conf;
2270 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2272 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2273 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2274 next unless $cust_bill_pkg->pkgnum > 0;
2276 foreach my $classnum ( keys %usage_class ) {
2277 my $section = $usage_class{$classnum}->classname;
2278 $classnums{$section} = $classnum;
2280 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2281 my $amount = $detail->amount;
2282 next unless $amount && $amount > 0;
2284 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2285 $sections{$section}{amount} += $amount; #subtotal
2286 $sections{$section}{calls}++;
2287 $sections{$section}{duration} += $detail->duration;
2289 my $desc = $detail->regionname;
2290 my $description = $desc;
2291 $description = substr($desc, 0, $maxlength). '...'
2292 if $format eq 'latex' && length($desc) > $maxlength;
2294 $lines{$section}{$desc} ||= {
2295 description => &{$escape}($description),
2296 #pkgpart => $part_pkg->pkgpart,
2297 pkgnum => $cust_bill_pkg->pkgnum,
2302 #unit_amount => $cust_bill_pkg->unitrecur,
2303 quantity => $cust_bill_pkg->quantity,
2304 product_code => 'N/A',
2305 ext_description => [],
2308 $lines{$section}{$desc}{amount} += $amount;
2309 $lines{$section}{$desc}{calls}++;
2310 $lines{$section}{$desc}{duration} += $detail->duration;
2316 my %sectionmap = ();
2317 foreach (keys %sections) {
2318 my $usage_class = $usage_class{$classnums{$_}};
2319 $sectionmap{$_} = { 'description' => &{$escape}($_),
2320 'amount' => $sections{$_}{amount}, #subtotal
2321 'calls' => $sections{$_}{calls},
2322 'duration' => $sections{$_}{duration},
2324 'tax_section' => '',
2325 'sort_weight' => $usage_class->weight,
2326 ( $usage_class->format
2327 ? ( map { $_ => $usage_class->$_($format) }
2328 qw( description_generator header_generator total_generator total_line_generator )
2335 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2339 foreach my $section ( keys %lines ) {
2340 foreach my $line ( keys %{$lines{$section}} ) {
2341 my $l = $lines{$section}{$line};
2342 $l->{section} = $sectionmap{$section};
2343 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2344 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2349 return(\@sections, \@lines);
2355 my $end = $self->_date;
2357 # start at date of previous invoice + 1 second or 0 if no previous invoice
2358 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2359 $start = 0 if !$start;
2362 my $cust_main = $self->cust_main;
2363 my @pkgs = $cust_main->all_pkgs;
2364 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2367 foreach my $pkg ( @pkgs ) {
2368 my @h_cust_svc = $pkg->h_cust_svc($end);
2369 foreach my $h_cust_svc ( @h_cust_svc ) {
2370 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2371 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2373 my $inserted = $h_cust_svc->date_inserted;
2374 my $deleted = $h_cust_svc->date_deleted;
2375 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2377 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
2379 # DID either activated or ported in; cannot be both for same DID simultaneously
2380 if ($inserted >= $start && $inserted <= $end && $phone_inserted
2381 && (!$phone_inserted->lnp_status
2382 || $phone_inserted->lnp_status eq ''
2383 || $phone_inserted->lnp_status eq 'native')) {
2386 else { # this one not so clean, should probably move to (h_)svc_phone
2387 local($FS::Record::qsearch_qualify_columns) = 0;
2388 my $phone_portedin = qsearchs( 'h_svc_phone',
2389 { 'svcnum' => $h_cust_svc->svcnum,
2390 'lnp_status' => 'portedin' },
2391 FS::h_svc_phone->sql_h_searchs($end),
2393 $num_portedin++ if $phone_portedin;
2396 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2397 if($deleted >= $start && $deleted <= $end && $phone_deleted
2398 && (!$phone_deleted->lnp_status
2399 || $phone_deleted->lnp_status ne 'portingout')) {
2402 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2403 && $phone_deleted->lnp_status
2404 && $phone_deleted->lnp_status eq 'portingout') {
2408 # increment usage minutes
2409 if ( $phone_inserted ) {
2410 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2411 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2414 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2417 # don't look at this service again
2418 push @seen, $h_cust_svc->svcnum;
2422 $minutes = sprintf("%d", $minutes);
2423 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
2424 . "$num_deactivated Ported-Out: $num_portedout ",
2425 "Total Minutes: $minutes");
2428 sub _items_accountcode_cdr {
2433 my $section = { 'amount' => 0,
2436 'sort_weight' => '',
2438 'description' => 'Usage by Account Code',
2444 my %accountcodes = ();
2446 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2447 next unless $cust_bill_pkg->pkgnum > 0;
2449 my @header = $cust_bill_pkg->details_header;
2450 next unless scalar(@header);
2451 $section->{'header'} = join(',',@header);
2453 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2455 $section->{'header'} = $detail->formatted('format' => $format)
2456 if($detail->detail eq $section->{'header'});
2458 my $accountcode = $detail->accountcode;
2459 next unless $accountcode;
2461 my $amount = $detail->amount;
2462 next unless $amount && $amount > 0;
2464 $accountcodes{$accountcode} ||= {
2465 description => $accountcode,
2472 product_code => 'N/A',
2473 section => $section,
2474 ext_description => [ $section->{'header'} ],
2478 $section->{'amount'} += $amount;
2479 $accountcodes{$accountcode}{'amount'} += $amount;
2480 $accountcodes{$accountcode}{calls}++;
2481 $accountcodes{$accountcode}{duration} += $detail->duration;
2482 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2486 foreach my $l ( values %accountcodes ) {
2487 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2488 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2489 foreach my $sorted_detail ( @sorted_detail ) {
2490 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2492 delete $l->{detail_temp};
2496 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2498 return ($section,\@sorted_lines);
2501 sub _items_svc_phone_sections {
2503 my $conf = $self->conf;
2511 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2513 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2514 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2516 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2517 next unless $cust_bill_pkg->pkgnum > 0;
2519 my @header = $cust_bill_pkg->details_header;
2520 next unless scalar(@header);
2522 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2524 my $phonenum = $detail->phonenum;
2525 next unless $phonenum;
2527 my $amount = $detail->amount;
2528 next unless $amount && $amount > 0;
2530 $sections{$phonenum} ||= { 'amount' => 0,
2533 'sort_weight' => -1,
2534 'phonenum' => $phonenum,
2536 $sections{$phonenum}{amount} += $amount; #subtotal
2537 $sections{$phonenum}{calls}++;
2538 $sections{$phonenum}{duration} += $detail->duration;
2540 my $desc = $detail->regionname;
2541 my $description = $desc;
2542 $description = substr($desc, 0, $maxlength). '...'
2543 if $format eq 'latex' && length($desc) > $maxlength;
2545 $lines{$phonenum}{$desc} ||= {
2546 description => &{$escape}($description),
2547 #pkgpart => $part_pkg->pkgpart,
2555 product_code => 'N/A',
2556 ext_description => [],
2559 $lines{$phonenum}{$desc}{amount} += $amount;
2560 $lines{$phonenum}{$desc}{calls}++;
2561 $lines{$phonenum}{$desc}{duration} += $detail->duration;
2563 my $line = $usage_class{$detail->classnum}->classname;
2564 $sections{"$phonenum $line"} ||=
2568 'sort_weight' => $usage_class{$detail->classnum}->weight,
2569 'phonenum' => $phonenum,
2570 'header' => [ @header ],
2572 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
2573 $sections{"$phonenum $line"}{calls}++;
2574 $sections{"$phonenum $line"}{duration} += $detail->duration;
2576 $lines{"$phonenum $line"}{$desc} ||= {
2577 description => &{$escape}($description),
2578 #pkgpart => $part_pkg->pkgpart,
2586 product_code => 'N/A',
2587 ext_description => [],
2590 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2591 $lines{"$phonenum $line"}{$desc}{calls}++;
2592 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2593 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2594 $detail->formatted('format' => $format);
2599 my %sectionmap = ();
2600 my $simple = new FS::usage_class { format => 'simple' }; #bleh
2601 foreach ( keys %sections ) {
2602 my @header = @{ $sections{$_}{header} || [] };
2604 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2605 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2606 my $usage_class = $summary ? $simple : $usage_simple;
2607 my $ending = $summary ? ' usage charges' : '';
2610 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2612 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2613 'amount' => $sections{$_}{amount}, #subtotal
2614 'calls' => $sections{$_}{calls},
2615 'duration' => $sections{$_}{duration},
2617 'tax_section' => '',
2618 'phonenum' => $sections{$_}{phonenum},
2619 'sort_weight' => $sections{$_}{sort_weight},
2620 'post_total' => $summary, #inspire pagebreak
2622 ( map { $_ => $usage_class->$_($format, %gen_opt) }
2623 qw( description_generator
2626 total_line_generator
2633 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2634 $a->{sort_weight} <=> $b->{sort_weight}
2639 foreach my $section ( keys %lines ) {
2640 foreach my $line ( keys %{$lines{$section}} ) {
2641 my $l = $lines{$section}{$line};
2642 $l->{section} = $sectionmap{$section};
2643 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2644 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2649 if($conf->exists('phone_usage_class_summary')) {
2650 # this only works with Latex
2654 # after this, we'll have only two sections per DID:
2655 # Calls Summary and Calls Detail
2656 foreach my $section ( @sections ) {
2657 if($section->{'post_total'}) {
2658 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2659 $section->{'total_line_generator'} = sub { '' };
2660 $section->{'total_generator'} = sub { '' };
2661 $section->{'header_generator'} = sub { '' };
2662 $section->{'description_generator'} = '';
2663 push @newsections, $section;
2664 my %calls_detail = %$section;
2665 $calls_detail{'post_total'} = '';
2666 $calls_detail{'sort_weight'} = '';
2667 $calls_detail{'description_generator'} = sub { '' };
2668 $calls_detail{'header_generator'} = sub {
2669 return ' & Date/Time & Called Number & Duration & Price'
2670 if $format eq 'latex';
2673 $calls_detail{'description'} = 'Calls Detail: '
2674 . $section->{'phonenum'};
2675 push @newsections, \%calls_detail;
2679 # after this, each usage class is collapsed/summarized into a single
2680 # line under the Calls Summary section
2681 foreach my $newsection ( @newsections ) {
2682 if($newsection->{'post_total'}) { # this means Calls Summary
2683 foreach my $section ( @sections ) {
2684 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
2685 && !$section->{'post_total'});
2686 my $newdesc = $section->{'description'};
2687 my $tn = $section->{'phonenum'};
2688 $newdesc =~ s/$tn//g;
2689 my $line = { ext_description => [],
2693 calls => $section->{'calls'},
2694 section => $newsection,
2695 duration => $section->{'duration'},
2696 description => $newdesc,
2697 amount => sprintf("%.2f",$section->{'amount'}),
2698 product_code => 'N/A',
2700 push @newlines, $line;
2705 # after this, Calls Details is populated with all CDRs
2706 foreach my $newsection ( @newsections ) {
2707 if(!$newsection->{'post_total'}) { # this means Calls Details
2708 foreach my $line ( @lines ) {
2709 next unless (scalar(@{$line->{'ext_description'}}) &&
2710 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2712 my @extdesc = @{$line->{'ext_description'}};
2714 foreach my $extdesc ( @extdesc ) {
2715 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2716 push @newextdesc, $extdesc;
2718 $line->{'ext_description'} = \@newextdesc;
2719 $line->{'section'} = $newsection;
2720 push @newlines, $line;
2725 return(\@newsections, \@newlines);
2728 return(\@sections, \@lines);
2732 =item _items_usage_class_summary OPTIONS
2734 Returns a list of detail items summarizing the usage charges on this
2735 invoice. Each one will have 'amount', 'description' (the usage charge name),
2736 and 'usage_classnum'.
2738 OPTIONS can include 'escape' (a function to escape the descriptions).
2742 sub _items_usage_class_summary {
2746 my $escape = $opt{escape} || sub { $_[0] };
2747 my $money_char = $opt{money_char};
2748 my $invnum = $self->invnum;
2749 my @classes = qsearch({
2750 'table' => 'usage_class',
2751 'select' => 'classnum, classname, SUM(amount) AS amount,'.
2752 ' COUNT(*) AS calls, SUM(duration) AS duration',
2753 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2754 ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2755 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2756 ' GROUP BY classnum, classname, weight'.
2757 ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2758 ' ORDER BY weight ASC',
2762 description => &{$escape}($self->mt('Usage Summary')),
2766 foreach my $class (@classes) {
2767 $section->{subtotal} += $class->get('amount');
2769 'description' => &{$escape}($class->classname),
2770 'amount' => $money_char.sprintf('%.2f', $class->get('amount')),
2771 'quantity' => $class->get('calls'),
2772 'duration' => $class->get('duration'),
2773 'usage_classnum' => $class->classnum,
2774 'section' => $section,
2777 $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
2781 =item _items_previous()
2783 Returns an array of hashrefs, each hashref representing a line-item on
2784 the current bill for previous unpaid invoices.
2786 keys for each previous_item:
2787 - amount (see notes)
2793 Payments and credits shown on this invoice may vary based on configuraiton.
2795 when conf flag previous_balance-payments_since is set:
2796 This method works backwards to rebuild the invoice as a snapshot in time.
2797 The invoice displayed will have the balances owed, and payments made,
2798 reflecting the state of the account at the time of invoice generation.
2802 sub _items_previous {
2807 if ($self->get('_items_previous')) {
2808 return sort { $a->{_date} <=> $b->{_date} }
2809 values %{ $self->get('_items_previous') };
2812 # Gets the customer's current balance and outstanding invoices.
2813 my ($prev_balance, @open_invoices) = $self->previous;
2815 my %invoices = map {
2816 $_->invnum => $self->__items_previous_map_invoice($_)
2819 # Which credits and payments displayed on the bill will vary based on
2820 # conf flag previous_balance-payments_since.
2821 my @credits = $self->_items_credits();
2822 my @payments = $self->_items_payments();
2825 if ($self->conf->exists('previous_balance-payments_since')) {
2826 # For each credit or payment, determine which invoices it was applied to.
2827 # Manipulate data displayed so the invoice displayed appears as a
2828 # snapshot in time... with previous balances and balance owed displayed
2829 # as they were at the time of invoice creation.
2831 my @credits_postbill = $self->_items_credits_postbill();
2832 my @payments_postbill = $self->_items_payments_postbill();
2837 # Each section below follows this pattern on a payment/credit
2839 # - Dupe check, avoid adjusting for the same item twice
2840 # - If invoice being adjusted for isn't in our list, add it
2841 # - Adjust the invoice balance to refelct balnace without the
2842 # credit or payment applied
2845 # Working with payments displayed on this bill
2846 for my $pmt_hash (@payments) {
2847 my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
2848 for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
2849 next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
2850 $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
2852 my $invnum = $cust_bill_pay->invnum;
2854 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2855 unless exists $invoices{$invnum};
2857 $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
2861 # Working with credits displayed on this bill
2862 for my $cred_hash (@credits) {
2863 my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
2864 for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
2865 next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
2866 $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
2868 my $invnum = $cust_credit_bill->invnum;
2870 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2871 unless exists $invoices{$invnum};
2873 $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
2877 # Working with both credits and payments which are not displayed
2878 # on this bill, but which have affected this bill's balances
2879 for my $postbill (@payments_postbill, @credits_postbill) {
2881 if ($postbill->{billpaynum}) {
2882 next if exists $pmnt_dupechk{$postbill->{billpaynum}};
2883 $pmnt_dupechk{$postbill->{billpaynum}} = 1;
2884 } elsif ($postbill->{creditbillnum}) {
2885 next if exists $cred_dupechk{$postbill->{creditbillnum}};
2886 $cred_dupechk{$postbill->{creditbillnum}} = 1;
2888 die "Missing creditbillnum or billpaynum";
2891 my $invnum = $postbill->{invnum};
2893 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2894 unless exists $invoices{$invnum};
2896 $invoices{$invnum}->{amount} += $postbill->{amount};
2899 # Make sure current invoice doesn't appear in previous items
2900 delete $invoices{$self->invnum}
2901 if exists $invoices{$self->invnum};
2905 # Make sure amount is formatted as a dollar string
2906 # (Formatting should happen on the template side, but is not?)
2907 $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
2910 $self->set('_items_previous', \%invoices);
2911 return sort { $a->{_date} <=> $b->{_date} } values %invoices;
2915 =item _items_previous_total
2917 Return sum of amounts from all items returned by _items_previous
2918 Results will vary based on invoicing conf flags
2922 sub _items_previous_total {
2925 $tot += $_->{amount} for $self->_items_previous();
2929 sub __items_previous_get_invoice {
2930 # Helper function for _items_previous
2932 # Read a record from cust_bill, return a hash of it's information
2933 my ($self, $invnum) = @_;
2934 die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
2936 my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
2937 return $self->__items_previous_map_invoice($cust_bill);
2940 sub __items_previous_map_invoice {
2941 # Helper function for _items_previous
2943 # Transform a cust_bill object into a simple hash reference of the type
2944 # required by _items_previous
2945 my ($self, $cust_bill) = @_;
2946 die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
2948 my $date = $self->conf->exists('invoice_show_prior_due_date')
2949 ? 'due '.$cust_bill->due_date2str('short')
2950 : $self->time2str_local('short', $cust_bill->_date);
2953 invnum => $cust_bill->invnum,
2954 amount => $cust_bill->owed,
2956 _date => $cust_bill->_date,
2957 description => join(' ',
2958 $self->mt('Previous Balance, Invoice #'),
2965 =item _items_credits()
2967 Return array of hashrefs containing credits to be shown as line-items
2968 when rendering this bill.
2970 keys for each credit item:
2971 - crednum: id of payment
2972 - amount: payment amount
2973 - description: line item to be displayed on the bill
2975 This method has three ways it selects which credits to display on
2978 1) Default Case: No Conf flag for 'previous_balance-payments_since'
2980 Returns credits that have been applied to this bill only
2983 Conf flag set for 'previous_balance-payments_since'
2985 List all credits that have been recorded during the time period
2986 between the timestamps of the last invoice and this invoice
2989 Conf flag set for 'previous_balance-payments_since'
2990 $opt{'template'} eq 'statement'
2992 List all payments that have been recorded between the timestamps
2993 of the previous invoice and the following invoice.
2995 This is used to give the customer a receipt for a payment
2996 in the form of their last bill with the payment amended.
2998 I am concerned with this implementation, but leaving in place as is
2999 If this option is selected, while viewing an older bill, the old bill
3000 will show ALL future credits for future bills, but no charges for
3001 future bills. Somebody could be misled into believing they have a
3002 large account credit when they don't. Also, interrupts the chain of
3003 invoices as an account history... the customer could have two invoices
3004 in their fileing cabinet, for two different dates, both with a line item
3005 for the same duplicate credit. The accounting is technically accurate,
3006 but somebody could easily become confused and think two credits were
3007 made, when really those two line items on two different bills represent
3008 only a single credit
3012 sub _items_credits {
3017 return @{$self->get('_items_credits')} if $self->get('_items_credits');
3020 my $template = $opt{template} || $self->get('_template');
3021 my $trim_len = $opt{template} || $self->get('trim_len') || 40;
3024 my @cust_credit_objs;
3026 if ( $self->conf->exists('previous_balance-payments_since') ) {
3027 if ($template eq 'statement') {
3028 # Case 3 (see above)
3029 # Return credits timestamped between the previous and following bills
3031 my $previous_bill = $self->previous_bill;
3032 my $following_bill = $self->following_bill;
3034 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3035 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3038 table => 'cust_credit',
3040 custnum => $self->custnum,
3041 _date => { op => '>=', value => $date_start },
3044 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3046 @cust_credit_objs = qsearch(\%query);
3049 # Case 2 (see above)
3050 # Return credits timestamps between this and the previous bills
3053 my $date_end = $self->_date;
3055 my $previous_bill = $self->previous_bill;
3056 if (ref $previous_bill) {
3057 $date_start = $previous_bill->_date;
3060 @cust_credit_objs = qsearch({
3061 table => 'cust_credit',
3063 custnum => $self->custnum,
3064 _date => {op => '>=', value => $date_start},
3066 extra_sql => " AND _date <= $date_end ",
3071 # Case 1 (see above)
3072 # Return only credits that have been applied to this bill
3074 @cust_credit_objs = $self->cust_credited;
3078 # Translate objects into hashrefs
3079 foreach my $obj ( @cust_credit_objs ) {
3080 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3082 amount => sprintf('%.2f',$cust_credit->amount),
3083 crednum => $cust_credit->crednum,
3084 _date => $cust_credit->_date,
3085 creditreason => $cust_credit->reason,
3088 my $reason = substr($cust_credit->reason, 0, $trim_len);
3089 $reason .= '...' if length($reason) < length($cust_credit->reason);
3090 $reason = " ($reason) " if $reason;
3092 $r_obj{description} = join(' ',
3093 $self->mt('Credit applied'),
3094 $self->time2str_local('short', $cust_credit->_date),
3098 push @return, \%r_obj;
3100 $self->set('_items_credits',\@return);
3104 =item _items_credits_total
3106 Return the total of al items from _items_credits
3107 Will vary based on invoice display conf flag
3111 sub _items_credits_total {
3114 $tot += $_->{amount} for $self->_items_credits();
3120 =item _items_credits_postbill()
3122 Returns an array of hashrefs for credits where
3123 - Credit issued after this invoice
3124 - Credit applied to an invoice before this invoice
3126 Returned hashrefs are of the format returned by _items_credits()
3130 sub _items_credits_postbill {
3133 my @cust_credit_bill = qsearch({
3134 table => 'cust_credit_bill',
3135 select => join(', ',qw(
3136 cust_credit_bill.creditbillnum
3137 cust_credit_bill._date
3138 cust_credit_bill.invnum
3139 cust_credit_bill.amount
3141 addl_from => ' LEFT JOIN cust_credit'.
3142 ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
3143 extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum.
3144 ' AND cust_credit_bill._date > '.$self->_date.
3145 ' AND cust_credit_bill.invnum < '.$self->invnum.' ',
3146 #! did not investigate why hashref doesn't work for this join query
3148 # 'cust_credit.custnum' => {op => '=', value => $self->custnum},
3149 # 'cust_credit_bill._date' => {op => '>', value => $self->_date},
3150 # 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
3156 invnum => $_->invnum,
3157 amount => $_->amount,
3158 creditbillnum => $_->creditbillnum,
3159 }} @cust_credit_bill;
3162 =item _items_payments_postbill()
3164 Returns an array of hashrefs for payments where
3165 - Payment occured after this invoice
3166 - Payment applied to an invoice before this invoice
3168 Returned hashrefs are of the format returned by _items_payments()
3172 sub _items_payments_postbill {
3175 my @cust_bill_pay = qsearch({
3176 table => 'cust_bill_pay',
3177 select => join(', ',qw(
3178 cust_bill_pay.billpaynum
3180 cust_bill_pay.invnum
3181 cust_bill_pay.amount
3183 addl_from => ' LEFT JOIN cust_bill'.
3184 ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
3185 extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum.
3186 ' AND cust_bill_pay._date > '.$self->_date.
3187 ' AND cust_bill_pay.invnum < '.$self->invnum.' ',
3192 invnum => $_->invnum,
3193 amount => $_->amount,
3194 billpaynum => $_->billpaynum,
3198 =item _items_payments()
3200 Return array of hashrefs containing payments to be shown as line-items
3201 when rendering this bill.
3203 keys for each payment item:
3204 - paynum: id of payment
3205 - amount: payment amount
3206 - description: line item to be displayed on the bill
3208 This method has three ways it selects which payments to display on
3211 1) Default Case: No Conf flag for 'previous_balance-payments_since'
3213 Returns payments that have been applied to this bill only
3216 Conf flag set for 'previous_balance-payments_since'
3218 List all payments that have been recorded between the timestamps
3219 of the previous invoice and this invoice
3222 Conf flag set for 'previous_balance-payments_since'
3223 $opt{'template'} eq 'statement'
3225 List all payments that have been recorded between the timestamps
3226 of the previous invoice and the following invoice.
3228 I am concerned with this implementation, but leaving in place as is
3229 If this option is selected, while viewing an older bill, the old bill
3230 will show ALL future payments for future bills, but no charges for
3231 future bills. Somebody could be misled into believing they have a
3232 large account credit when they don't. Also, interrupts the chain of
3233 invoices as an account history... the customer could have two invoices
3234 in their fileing cabinet, for two different dates, both with a line item
3235 for the same duplicate payment. The accounting is technically accurate,
3236 but somebody could easily become confused and think two payments were
3237 made, when really those two line items on two different bills represent
3238 only a single payment.
3242 sub _items_payments {
3247 return @{$self->get('_items_payments')} if $self->get('_items_payments');
3250 my $template = $opt{template} || $self->get('_template');
3255 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3257 if ( $self->conf->exists('previous_balance-payments_since') ) {
3258 if ($template eq 'statement') {
3259 # Case 3 (see above)
3260 # Return payments timestamped between the previous and following bills
3262 my $previous_bill = $self->previous_bill;
3263 my $following_bill = $self->following_bill;
3265 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3266 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3269 table => 'cust_pay',
3271 custnum => $self->custnum,
3272 _date => { op => '>=', value => $date_start },
3275 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3277 @cust_pay_objs = qsearch(\%query);
3280 # Case 2 (see above)
3281 # Return payments timestamped between this and the previous bill
3284 my $date_end = $self->_date;
3286 my $previous_bill = $self->previous_bill;
3287 if (ref $previous_bill) {
3288 $date_start = $previous_bill->_date;
3291 @cust_pay_objs = qsearch({
3292 table => 'cust_pay',
3294 custnum => $self->custnum,
3295 _date => {op => '>=', value => $date_start},
3297 extra_sql => " AND _date <= $date_end ",
3302 # Case 1 (see above)
3303 # Return payments applied only to this bill
3305 @cust_pay_objs = $self->cust_bill_pay;
3311 [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
3313 return @{ $self->get('_items_payments') };
3316 =item _items_payments_total
3318 Return a total of all records returned by _items_payments
3319 Results vary based on invoicing conf flags
3323 sub _items_payments_total {
3326 $tot += $_->{amount} for $self->_items_payments();
3330 sub __items_payments_make_hashref {
3331 # Transform a FS::cust_pay object into a simple hashref for invoice
3332 my ($self, @cust_pay_objs) = @_;
3333 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3336 for my $obj (@cust_pay_objs) {
3338 # In case we're passed FS::cust_bill_pay (or something else?)
3339 # Below, we use $obj to render amount rather than $cust_apy.
3340 # If we were passed cust_bill_pay objs, then:
3341 # $obj->amount represents the amount applied to THIS invoice
3342 # $cust_pay->amount represents the total payment, which may have
3343 # been applied accross several invoices.
3344 # If we were passed cust_bill_pay objects, then the conf flag
3345 # previous_balance-payments_since is NOT set, so we should not
3346 # present any payments not applied to this invoice.
3347 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3350 _date => $cust_pay->_date,
3351 amount => sprintf("%.2f", $obj->amount),
3352 paynum => $cust_pay->paynum,
3353 payinfo => $cust_pay->payby_payinfo_pretty(),
3354 description => join(' ',
3355 $self->mt('Payment received'),
3356 $self->time2str_local('short', $cust_pay->_date),
3360 if ($c_invoice_payment_details) {
3361 $r_obj{description} = join(' ',
3362 $r_obj{description},
3364 $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
3368 push @return, \%r_obj;
3373 =item _items_total()
3375 Generate the line-items to be shown on the bill in the "Totals" section
3377 Returns a list of hashrefs, each with the keys:
3378 - total_item: description field
3379 - total_amount: dollar-formatted number amount
3381 Information presented by this method varies based on Conf
3383 Conf previous_balance-payments_due
3384 - default, flag not set
3385 Only transactions that were applied to this bill bill be
3386 displayed and calculated intothe total. If items exist in
3387 the past-due section, those items will disappear from this
3388 invoice if they have been paid off.
3390 - previous_balance-payments_due flag is set
3391 Transactions occuring after the timestsamp of this
3392 invoice are not reflected on invoice line items
3394 Only payments/credits applied between the previous invoice
3395 and this one are displayed and calculated into the total
3397 - previous_balance-payments_due && $opt{template} eq 'statement'
3398 Same as above, except payments/credits occuring before the date
3399 of the following invoice are also displayed and calculated into
3402 Conf previous_balance-exclude_from_total
3403 - default, flag not set
3404 The "Totals" section contains a single line item.
3405 The dollar amount of this line items is a sum of old and new charges
3406 - previous_balance-exclude_from_total flag is set
3407 The "Totals" section contains two line items.
3408 One for previous balance, one for new charges
3409 !NOTE: Avent virtualization flag 'disable_previous_balance' can
3410 override the global conf flag previous_balance-exclude_from_total
3412 Conf invoice_show_prior_due_date
3413 - default, flag not set
3414 Total line item in the "Totals" section does not mention due date
3415 - invoice_show_prior_due_date flag is set
3416 Total line item in the "Totals" section includes either the due
3417 date of the invoice, or the specified invoice terms
3418 ? Not sure why this is called "Prior" due date, since we seem to be
3419 displaying THIS due date...
3424 my $conf = $self->conf;
3426 my $c_multi_line_total = 0;
3427 $c_multi_line_total = 1
3428 if $conf->exists('previous_balance-exclude_from_total')
3429 && $self->enable_previous();
3432 my $invoice_charges = $self->charged();
3434 # _items_previous() is aware of conf flags
3435 my $previous_balance = 0;
3436 $previous_balance += $_->{amount} for $self->_items_previous();
3441 if ( $previous_balance && $c_multi_line_total ) {
3442 # previous balance, new charges on separate lines
3445 total_amount => sprintf('%.2f',$previous_balance),
3446 total_item => $self->mt(
3447 $conf->config('previous_balance-text') || 'Previous Balance'
3451 $total_charges = $invoice_charges;
3452 $total_descr = $self->mt(
3453 $conf->config('previous_balance-text-total_new_charges')
3454 || 'Total New Charges'
3458 # previous balance and new charges combined into a single total line
3459 $total_charges = $invoice_charges + $previous_balance;
3460 $total_descr = $self->mt('Total Charges');
3463 if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
3464 # then the due date should be shown with Total New Charges,
3465 # and should NOT be shown with the Balance Due message.
3467 if ( $self->due_date ) {
3468 $total_descr .= $self->invoice_pay_by_msg;
3469 } elsif ( $self->terms ) {
3470 $total_descr = join(' ',
3473 $self->mt($self->terms)
3479 total_amount => sprintf('%.2f', $total_charges),
3480 total_item => $total_descr,
3486 =item _items_aging_balances
3488 Returns an array of aged balance amounts from a given epoch timestamp.
3490 The time of day is ignored for this calculation, so that slight differences
3491 on the generation time of an invoice doesn't determine which column an
3492 aged balance falls into.
3494 Will not include any balances dated after the given timestamp in
3495 the calculated totals
3498 @aged_balances = $b->_items_aging_balances( $b->_date )
3509 sub _items_aging_balances {
3510 my ($self, $basetime) = @_;
3511 die "Incorrect usage of _items_aging_balances()" unless ref $self;
3513 $basetime = $self->_date unless $basetime;
3514 my @aging_balances = (0, 0, 0, 0);
3515 my @open_invoices = $self->_items_previous();
3516 my $d30 = 2592000; # 60 * 60 * 24 * 30,
3517 my $d60 = 5184000; # 60 * 60 * 24 * 60,
3518 my $d90 = 7776000; # 60 * 60 * 24 * 90
3520 # Move the clock back on our given day to 12:00:01 AM
3521 my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
3522 my $dt_12am = DateTime->new(
3523 year => $dt_basetime->year,
3524 month => $dt_basetime->month,
3525 day => $dt_basetime->day,
3531 # set our epoch breakpoints
3532 $_ = $dt_12am - $_ for $d30, $d60, $d90;
3534 # grep the aged balances
3535 for my $oinv (@open_invoices) {
3536 if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
3537 # If post invoice dated less than 30days ago
3538 $aging_balances[0] += $oinv->{amount};
3539 } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
3540 # If past invoice dated between 30-60 days ago
3541 $aging_balances[1] += $oinv->{amount};
3542 } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
3543 # If past invoice dated between 60-90 days ago
3544 $aging_balances[2] += $oinv->{amount};
3546 # If past invoice dated 90+ days ago
3547 $aging_balances[3] += $oinv->{amount};
3551 return map{ sprintf('%.2f',$_) } @aging_balances;
3554 =item has_call_details
3556 Returns true if this invoice has call details.
3560 sub has_call_details {
3563 SELECT 1 FROM cust_bill_pkg_detail
3564 LEFT JOIN cust_bill_pkg USING (billpkgnum)
3565 WHERE cust_bill_pkg_detail.format = 'C'
3566 AND cust_bill_pkg.invnum = ?
3571 =item call_details [ OPTION => VALUE ... ]
3573 Returns an array of CSV strings representing the call details for this invoice
3574 The only option available is the boolean prepend_billed_number
3579 my ($self, %opt) = @_;
3581 my $format_function = sub { shift };
3583 if ($opt{prepend_billed_number}) {
3584 $format_function = sub {
3588 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3593 my @details = map { $_->details( 'format_function' => $format_function,
3594 'escape_function' => sub{ return() },
3598 $self->cust_bill_pkg;
3599 my $header = $details[0];
3600 ( $header, grep { $_ ne $header } @details );
3603 =item cust_pay_batch
3605 Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
3610 sub cust_pay_batch {
3611 carp "FS::cust_bill->cust_pay_batch is deprecated";
3613 qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
3622 =item process_reprint
3626 sub process_reprint {
3627 process_re_X('print', @_);
3630 =item process_reemail
3634 sub process_reemail {
3635 process_re_X('email', @_);
3643 process_re_X('fax', @_);
3651 process_re_X('ftp', @_);
3658 sub process_respool {
3659 process_re_X('spool', @_);
3664 my( $method, $job ) = ( shift, shift );
3665 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3668 warn Dumper($param) if $DEBUG;
3678 # this is called from search/cust_bill.html and given all its search
3679 # parameters, so it needs to perform the same search.
3682 # spool_invoice ftp_invoice fax_invoice print_invoice
3683 my($method, $job, %param ) = @_;
3685 warn "re_X $method for job $job with param:\n".
3686 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3689 #some false laziness w/search/cust_bill.html
3690 $param{'order_by'} = 'cust_bill._date';
3692 my $query = FS::cust_bill->search(\%param);
3693 delete $query->{'count_query'};
3694 delete $query->{'count_addl'};
3696 $query->{debug} = 1; # was in here before, is obviously useful
3698 my @cust_bill = qsearch( $query );
3700 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3702 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3705 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3706 foreach my $cust_bill ( @cust_bill ) {
3707 $cust_bill->$method();
3709 if ( $job ) { #progressbar foo
3711 if ( time - $min_sec > $last ) {
3712 my $error = $job->update_statustext(
3713 int( 100 * $num / scalar(@cust_bill) )
3715 die $error if $error;
3726 +{ ( map { $_=>$self->$_ } $self->fields ),
3727 'owed' => $self->owed,
3728 #XXX last payment applied date
3734 =head1 CLASS METHODS
3740 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3745 my ($class, $start, $end) = @_;
3747 $class->paid_sql($start, $end). ' - '.
3748 $class->credited_sql($start, $end);
3753 Returns an SQL fragment to retreive the net amount (charged minus credited).
3758 my ($class, $start, $end) = @_;
3759 'charged - '. $class->credited_sql($start, $end);
3764 Returns an SQL fragment to retreive the amount paid against this invoice.
3769 my ($class, $start, $end) = @_;
3770 $start &&= "AND cust_bill_pay._date <= $start";
3771 $end &&= "AND cust_bill_pay._date > $end";
3772 $start = '' unless defined($start);
3773 $end = '' unless defined($end);
3774 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3775 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
3780 Returns an SQL fragment to retreive the amount credited against this invoice.
3785 my ($class, $start, $end) = @_;
3786 $start &&= "AND cust_credit_bill._date <= $start";
3787 $end &&= "AND cust_credit_bill._date > $end";
3788 $start = '' unless defined($start);
3789 $end = '' unless defined($end);
3790 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3791 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
3796 Returns an SQL fragment to retrieve the due date of an invoice.
3797 Currently only supported on PostgreSQL.
3802 die "don't use: doesn't account for agent-specific invoice_default_terms";
3804 #we're passed a $conf but not a specific customer (that's in the query), so
3805 # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3806 # "condition_sql_option" that retreives a conf value with SQL in an agent-
3809 my $conf = new FS::Conf;
3813 cust_bill.invoice_terms,
3814 cust_main.invoice_terms,
3815 \''.($conf->config('invoice_default_terms') || '').'\'
3816 ), E\'Net (\\\\d+)\'
3818 ) * 86400 + cust_bill._date'
3829 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3830 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base