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;
46 $me = '[FS::cust_bill]';
50 FS::cust_bill - Object methods for cust_bill records
56 $record = new FS::cust_bill \%hash;
57 $record = new FS::cust_bill { 'column' => 'value' };
59 $error = $record->insert;
61 $error = $new_record->replace($old_record);
63 $error = $record->delete;
65 $error = $record->check;
67 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
69 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
71 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
73 @cust_pay_objects = $cust_bill->cust_pay;
75 $tax_amount = $record->tax;
77 @lines = $cust_bill->print_text;
78 @lines = $cust_bill->print_text('time' => $time);
82 An FS::cust_bill object represents an invoice; a declaration that a customer
83 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
84 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
85 following fields are currently supported:
91 =item invnum - primary key (assigned automatically for new invoices)
93 =item custnum - customer (see L<FS::cust_main>)
95 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
96 L<Time::Local> and L<Date::Parse> for conversion functions.
98 =item charged - amount of this invoice
100 =item invoice_terms - optional terms override for this specific invoice
108 =item billing_balance - the customer's balance immediately before generating
109 this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
110 to determine the customer's balance at a specific time.
112 =item previous_balance - the customer's balance immediately after generating
113 the invoice before this one. DEPRECATED.
115 =item printed - formerly used to track the number of times an invoice had
116 been printed; no longer used.
124 =item closed - books closed flag, empty or `Y'
126 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
128 =item agent_invid - legacy invoice number
130 =item promised_date - customer promised payment date, for collection
132 =item pending - invoice is still being generated, empty or 'Y'
142 Creates a new invoice. To add the invoice to the database, see L<"insert">.
143 Invoices are normally created by calling the bill method of a customer object
144 (see L<FS::cust_main>).
148 sub table { 'cust_bill'; }
149 sub template_conf { 'invoice_'; }
153 my $agentnum = $self->cust_main->agentnum;
154 my $tc = $self->template_conf;
156 $self->conf->exists($tc.'sections', $agentnum) ||
157 $self->conf->exists($tc.'sections_by_location', $agentnum);
160 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
161 # (except email_subject and invnum_date_pretty)
164 $self->conf->config('notice_name') || 'Invoice'
167 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
168 sub cust_unlinked_msg {
170 "WARNING: can't find cust_main.custnum ". $self->custnum.
171 ' (cust_bill.invnum '. $self->invnum. ')';
176 Adds this invoice to the database ("Posts" the invoice). If there is an error,
177 returns the error, otherwise returns false.
183 warn "$me insert called\n" if $DEBUG;
185 local $SIG{HUP} = 'IGNORE';
186 local $SIG{INT} = 'IGNORE';
187 local $SIG{QUIT} = 'IGNORE';
188 local $SIG{TERM} = 'IGNORE';
189 local $SIG{TSTP} = 'IGNORE';
190 local $SIG{PIPE} = 'IGNORE';
192 my $oldAutoCommit = $FS::UID::AutoCommit;
193 local $FS::UID::AutoCommit = 0;
196 my $error = $self->SUPER::insert;
198 $dbh->rollback if $oldAutoCommit;
202 if ( $self->get('cust_bill_pkg') ) {
203 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
204 $cust_bill_pkg->invnum($self->invnum);
205 my $error = $cust_bill_pkg->insert;
207 $dbh->rollback if $oldAutoCommit;
208 return "can't create invoice line item: $error";
213 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 =item void [ REASON ]
220 Voids this invoice: deletes the invoice and adds a record of the voided invoice
221 to the FS::cust_bill_void table (and related tables starting from
222 FS::cust_bill_pkg_void).
228 my $reason = scalar(@_) ? shift : '';
230 unless (ref($reason) || !$reason) {
231 $reason = FS::reason->new_or_existing(
233 'type' => 'Invoice void',
238 local $SIG{HUP} = 'IGNORE';
239 local $SIG{INT} = 'IGNORE';
240 local $SIG{QUIT} = 'IGNORE';
241 local $SIG{TERM} = 'IGNORE';
242 local $SIG{TSTP} = 'IGNORE';
243 local $SIG{PIPE} = 'IGNORE';
245 my $oldAutoCommit = $FS::UID::AutoCommit;
246 local $FS::UID::AutoCommit = 0;
249 my $cust_bill_void = new FS::cust_bill_void ( {
250 map { $_ => $self->get($_) } $self->fields
252 $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
253 my $error = $cust_bill_void->insert;
255 $dbh->rollback if $oldAutoCommit;
259 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
260 my $error = $cust_bill_pkg->void($reason);
262 $dbh->rollback if $oldAutoCommit;
267 $error = $self->_delete;
269 $dbh->rollback if $oldAutoCommit;
273 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
279 # removed docs entirely and renamed method to _delete to further indicate it is
280 # internal-only and discourage use
284 # DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
285 # the B<void> method.
287 # This is only for internal use by V<void>, which is what you should be using.
289 # DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly
290 # wrong. Use B<void>, that's what it is for. Really. This means you.
296 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
298 local $SIG{HUP} = 'IGNORE';
299 local $SIG{INT} = 'IGNORE';
300 local $SIG{QUIT} = 'IGNORE';
301 local $SIG{TERM} = 'IGNORE';
302 local $SIG{TSTP} = 'IGNORE';
303 local $SIG{PIPE} = 'IGNORE';
305 my $oldAutoCommit = $FS::UID::AutoCommit;
306 local $FS::UID::AutoCommit = 0;
309 foreach my $table (qw(
316 #cust_event # problematic
317 #cust_pay_batch # unnecessary
319 foreach my $linked ( $self->$table() ) {
320 my $error = $linked->delete;
322 $dbh->rollback if $oldAutoCommit;
329 my $error = $self->SUPER::delete(@_);
331 $dbh->rollback if $oldAutoCommit;
335 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
341 =item replace [ OLD_RECORD ]
343 You can, but probably shouldn't modify invoices...
345 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
346 supplied, replaces this record. If there is an error, returns the error,
347 otherwise returns false.
351 #replace can be inherited from Record.pm
353 # replace_check is now the preferred way to #implement replace data checks
354 # (so $object->replace() works without an argument)
357 my( $new, $old ) = ( shift, shift );
358 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
359 #return "Can't change _date!" unless $old->_date eq $new->_date;
360 return "Can't change _date" unless $old->_date == $new->_date;
361 return "Can't change charged" unless $old->charged == $new->charged
362 || $old->pending eq 'Y'
363 || $old->charged == 0
364 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
370 =item add_cc_surcharge
376 sub add_cc_surcharge {
377 my ($self, $pkgnum, $amount) = (shift, shift, shift);
380 my $cust_bill_pkg = new FS::cust_bill_pkg({
381 'invnum' => $self->invnum,
385 $error = $cust_bill_pkg->insert;
386 return $error if $error;
388 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
389 $self->charged($self->charged+$amount);
390 $error = $self->replace;
391 return $error if $error;
393 $self->apply_payments_and_credits;
399 Checks all fields to make sure this is a valid invoice. If there is an error,
400 returns the error, otherwise returns false. Called by the insert and replace
409 $self->ut_numbern('invnum')
410 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
411 || $self->ut_numbern('_date')
412 || $self->ut_money('charged')
413 || $self->ut_numbern('printed')
414 || $self->ut_enum('closed', [ '', 'Y' ])
415 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
416 || $self->ut_numbern('agent_invid') #varchar?
417 || $self->ut_flag('pending')
419 return $error if $error;
421 $self->_date(time) unless $self->_date;
423 $self->printed(0) if $self->printed eq '';
430 Returns the displayed invoice number for this invoice: agent_invid if
431 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
437 if ( $self->agent_invid
438 && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
439 return $self->agent_invid;
441 return $self->invnum;
447 Returns the customer's last invoice before this one.
453 if ( !$self->get('previous_bill') ) {
454 $self->set('previous_bill', qsearchs({
455 'table' => 'cust_bill',
456 'hashref' => { 'custnum' => $self->custnum,
457 '_date' => { op=>'<', value=>$self->_date } },
458 'order_by' => 'ORDER BY _date DESC LIMIT 1',
461 $self->get('previous_bill');
466 Returns the customer's invoice that follows this one
472 if (!$self->get('following_bill')) {
473 $self->set('following_bill', qsearchs({
474 table => 'cust_bill',
476 custnum => $self->custnum,
477 invnum => { op => '>', value => $self->invnum },
479 order_by => 'ORDER BY invnum ASC LIMIT 1',
482 $self->get('following_bill');
487 Returns a list consisting of the total previous balance for this customer,
488 followed by the previous outstanding invoices (as FS::cust_bill objects also).
494 # simple memoize; we use this a lot
495 if (!$self->get('previous')) {
497 my @cust_bill = sort { $a->_date <=> $b->_date }
498 grep { $_->owed != 0 }
499 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
500 #'_date' => { op=>'<', value=>$self->_date },
501 'invnum' => { op=>'<', value=>$self->invnum },
504 foreach ( @cust_bill ) { $total += $_->owed; }
505 $self->set('previous', [$total, @cust_bill]);
507 return @{ $self->get('previous') };
510 =item enable_previous
512 Whether to show the 'Previous Charges' section when printing this invoice.
513 The negation of the 'disable_previous_balance' config setting.
517 sub enable_previous {
519 my $agentnum = $self->cust_main->agentnum;
520 !$self->conf->exists('disable_previous_balance', $agentnum);
525 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
532 { 'table' => 'cust_bill_pkg',
533 'hashref' => { 'invnum' => $self->invnum },
534 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
535 # the AUTLOADED FK search. or should
536 # that default to ORDER by the pkey?
541 =item cust_bill_pkg_pkgnum PKGNUM
543 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
548 sub cust_bill_pkg_pkgnum {
549 my( $self, $pkgnum ) = @_;
551 { 'table' => 'cust_bill_pkg',
552 'hashref' => { 'invnum' => $self->invnum,
555 'order_by' => 'ORDER BY billpkgnum',
562 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
569 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
570 $self->cust_bill_pkg;
572 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
577 Returns true if any of the packages (or their definitions) corresponding to the
578 line items for this invoice have the no_auto flag set.
584 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
587 =item open_cust_bill_pkg
589 Returns the open line items for this invoice.
591 Note that cust_bill_pkg with both setup and recur fees are returned as two
592 separate line items, each with only one fee.
596 # modeled after cust_main::open_cust_bill
597 sub open_cust_bill_pkg {
600 # grep { $_->owed > 0 } $self->cust_bill_pkg
602 my %other = ( 'recur' => 'setup',
603 'setup' => 'recur', );
605 foreach my $field ( qw( recur setup )) {
606 push @open, map { $_->set( $other{$field}, 0 ); $_; }
607 grep { $_->owed($field) > 0 }
608 $self->cust_bill_pkg;
616 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
620 #false laziness w/cust_pkg.pm
624 'table' => 'cust_event',
625 'addl_from' => 'JOIN part_event USING ( eventpart )',
626 'hashref' => { 'tablenum' => $self->invnum },
627 'extra_sql' => " AND eventtable = 'cust_bill' ",
633 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
637 #false laziness w/cust_pkg.pm
641 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
642 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
643 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
644 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
645 $sth->fetchrow_arrayref->[0];
650 Returns the customer (see L<FS::cust_main>) for this invoice.
654 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
656 Returns a list: an empty list on success or a list of errors.
663 grep { $_->suspend(@_) }
664 grep {! $_->getfield('cancel') }
669 =item cust_suspend_if_balance_over AMOUNT
671 Suspends the customer associated with this invoice if the total amount owed on
672 this invoice and all older invoices is greater than the specified amount.
674 Returns a list: an empty list on success or a list of errors.
678 sub cust_suspend_if_balance_over {
679 my( $self, $amount ) = ( shift, shift );
680 my $cust_main = $self->cust_main;
681 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
684 $cust_main->suspend(@_);
690 Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
695 my( $self, %opt ) = @_;
697 warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
698 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
701 return ( 'Access denied' )
702 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
704 my @pkgs = $self->cust_pkg;
706 if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
708 my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
709 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
714 map { $_->cancel(%opt) }
715 grep { ! $_->getfield('cancel') }
721 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
727 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
728 sort { $a->_date <=> $b->_date }
729 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
734 =item cust_credit_bill
736 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
742 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
743 sort { $a->_date <=> $b->_date }
744 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
748 sub cust_credit_bill {
749 shift->cust_credited(@_);
752 #=item cust_bill_pay_pkgnum PKGNUM
754 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
755 #with matching pkgnum.
759 #sub cust_bill_pay_pkgnum {
760 # my( $self, $pkgnum ) = @_;
761 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
762 # sort { $a->_date <=> $b->_date }
763 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
764 # 'pkgnum' => $pkgnum,
769 =item cust_bill_pay_pkg PKGNUM
771 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
772 applied against the matching pkgnum.
776 sub cust_bill_pay_pkg {
777 my( $self, $pkgnum ) = @_;
780 'select' => 'cust_bill_pay_pkg.*',
781 'table' => 'cust_bill_pay_pkg',
782 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
783 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
784 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
785 " AND cust_bill_pkg.pkgnum = $pkgnum",
790 #=item cust_credited_pkgnum PKGNUM
792 #=item cust_credit_bill_pkgnum PKGNUM
794 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
795 #with matching pkgnum.
799 #sub cust_credited_pkgnum {
800 # my( $self, $pkgnum ) = @_;
801 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
802 # sort { $a->_date <=> $b->_date }
803 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
804 # 'pkgnum' => $pkgnum,
809 #sub cust_credit_bill_pkgnum {
810 # shift->cust_credited_pkgnum(@_);
813 =item cust_credit_bill_pkg PKGNUM
815 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
816 applied against the matching pkgnum.
820 sub cust_credit_bill_pkg {
821 my( $self, $pkgnum ) = @_;
824 'select' => 'cust_credit_bill_pkg.*',
825 'table' => 'cust_credit_bill_pkg',
826 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
827 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
828 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
829 " AND cust_bill_pkg.pkgnum = $pkgnum",
834 =item cust_bill_batch
836 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
840 sub cust_bill_batch {
842 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
847 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
848 hash keyed by term length.
854 FS::discount_plan->all($self);
859 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
866 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
868 foreach (@taxlines) { $total += $_->setup; }
874 Returns the amount owed (still outstanding) on this invoice, which is charged
875 minus all payment applications (see L<FS::cust_bill_pay>) and credit
876 applications (see L<FS::cust_credit_bill>).
882 my $balance = $self->charged;
883 $balance -= $_->amount foreach ( $self->cust_bill_pay );
884 $balance -= $_->amount foreach ( $self->cust_credited );
885 $balance = sprintf( "%.2f", $balance);
886 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
890 =item owed_on_invoice
892 Returns the amount to be displayed as the "Balance Due" on this
893 invoice. Amount returned depends on conf flags for invoicing
895 See L<FS::cust_bill::owed> for the true amount currently owed
899 sub owed_on_invoice {
902 #return $self->owed()
903 # unless $self->conf->exists('previous_balance-payments_since')
905 # Add charges from this invoice
906 my $owed = $self->charged();
908 # Add carried balances from previous invoices
909 # If previous items aren't to be displayed on the invoice,
910 # _items_previous() is aware of this and responds appropriately.
911 $owed += $_->{amount} for $self->_items_previous();
913 # Subtract payments and credits displayed on this invoice
914 $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
920 my( $self, $pkgnum ) = @_;
922 #my $balance = $self->charged;
924 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
926 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
927 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
929 $balance = sprintf( "%.2f", $balance);
930 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
936 Returns true if this invoice should be hidden. See the
937 selfservice-hide_invoices-taxclass configuraiton setting.
943 my $conf = $self->conf;
944 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
946 my @cust_bill_pkg = $self->cust_bill_pkg;
947 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
948 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
951 =item apply_payments_and_credits [ OPTION => VALUE ... ]
953 Applies unapplied payments and credits to this invoice.
954 Payments with the no_auto_apply flag set will not be applied.
956 A hash of optional arguments may be passed. Currently "manual" is supported.
957 If true, a payment receipt is sent instead of a statement when
958 'payment_receipt_email' configuration option is set.
960 If there is an error, returns the error, otherwise returns false.
964 sub apply_payments_and_credits {
965 my( $self, %options ) = @_;
966 my $conf = $self->conf;
968 local $SIG{HUP} = 'IGNORE';
969 local $SIG{INT} = 'IGNORE';
970 local $SIG{QUIT} = 'IGNORE';
971 local $SIG{TERM} = 'IGNORE';
972 local $SIG{TSTP} = 'IGNORE';
973 local $SIG{PIPE} = 'IGNORE';
975 my $oldAutoCommit = $FS::UID::AutoCommit;
976 local $FS::UID::AutoCommit = 0;
979 $self->select_for_update; #mutex
981 my @payments = grep { $_->unapplied > 0 }
982 grep { !$_->no_auto_apply }
983 $self->cust_main->cust_pay;
984 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
986 if ( $conf->exists('pkg-balances') ) {
987 # limit @payments & @credits to those w/ a pkgnum grepped from $self
988 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
989 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
990 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
993 while ( $self->owed > 0 and ( @payments || @credits ) ) {
996 if ( @payments && @credits ) {
998 #decide which goes first by weight of top (unapplied) line item
1000 my @open_lineitems = $self->open_cust_bill_pkg;
1002 my $max_pay_weight =
1003 max( map { $_->part_pkg->pay_weight || 0 }
1005 map { $_->cust_pkg }
1008 my $max_credit_weight =
1009 max( map { $_->part_pkg->credit_weight || 0 }
1011 map { $_->cust_pkg }
1015 #if both are the same... payments first? it has to be something
1016 if ( $max_pay_weight >= $max_credit_weight ) {
1022 } elsif ( @payments ) {
1024 } elsif ( @credits ) {
1027 die "guru meditation #12 and 35";
1031 if ( $app eq 'pay' ) {
1033 my $payment = shift @payments;
1034 $unapp_amount = $payment->unapplied;
1035 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
1036 $app->pkgnum( $payment->pkgnum )
1037 if $conf->exists('pkg-balances') && $payment->pkgnum;
1039 } elsif ( $app eq 'credit' ) {
1041 my $credit = shift @credits;
1042 $unapp_amount = $credit->credited;
1043 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
1044 $app->pkgnum( $credit->pkgnum )
1045 if $conf->exists('pkg-balances') && $credit->pkgnum;
1048 die "guru meditation #12 and 35";
1052 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
1053 warn "owed_pkgnum ". $app->pkgnum;
1054 $owed = $self->owed_pkgnum($app->pkgnum);
1056 $owed = $self->owed;
1058 next unless $owed > 0;
1060 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
1061 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
1063 $app->invnum( $self->invnum );
1065 my $error = $app->insert(%options);
1067 $dbh->rollback if $oldAutoCommit;
1068 return "Error inserting ". $app->table. " record: $error";
1070 die $error if $error;
1074 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1081 Sends this invoice to the destinations configured for this customer: sends
1082 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1084 Options can be passed as a hashref. Positional parameters are no longer
1087 I<template>: a suffix for alternate invoices
1089 I<agentnum>: obsolete, now does nothing.
1091 I<from> overrides the default email invoice From: address.
1093 I<amount>: obsolete, does nothing
1095 I<notice_name> overrides "Invoice" as the name of the sent document
1096 (templates from 10/2009 or newer required).
1098 I<lpr> overrides the system 'lpr' option as the command to print a document
1099 from standard input.
1105 my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1106 my $conf = $self->conf;
1108 my $cust_main = $self->cust_main;
1110 my @invoicing_list = $cust_main->invoicing_list;
1113 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1114 && ! $cust_main->invoice_noemail;
1117 if grep { $_ eq 'POST' } @invoicing_list; #postal
1119 #this has never been used post-$ORIGINAL_ISP afaik
1120 $self->fax_invoice($opt)
1121 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1129 my $opt = shift || {};
1130 if ($opt and !ref($opt)) {
1131 die ref($self). '->email called with positional parameters';
1134 my $conf = $self->conf;
1136 my $from = delete $opt->{from};
1138 # this is where we set the From: address
1139 $from ||= $self->_agent_invoice_from || #XXX should go away
1140 $conf->invoice_from_full( $self->cust_main->agentnum );
1142 my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1144 if ( ! @invoicing_list ) { #no recipients
1145 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1146 die 'No recipients for customer #'. $self->custnum;
1148 #default: better to notify this person than silence
1149 @invoicing_list = ($from);
1153 $self->SUPER::email( {
1155 'to' => \@invoicing_list,
1161 #this stays here for now because its explicitly used as
1162 # FS::cust_bill::queueable_email
1163 sub queueable_email {
1166 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1167 or die "invalid invoice number: " . $opt{invnum};
1169 $self->set('mode', $opt{mode})
1172 my %args = map {$_ => $opt{$_}}
1174 qw( from notice_name no_coupon template );
1176 my $error = $self->email( \%args );
1177 die $error if $error;
1183 my $conf = $self->conf;
1185 #my $template = scalar(@_) ? shift : '';
1188 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1191 my $cust_main = $self->cust_main;
1192 my $name = $cust_main->name;
1193 my $name_short = $cust_main->name_short;
1194 my $invoice_number = $self->invnum;
1195 my $invoice_date = $self->_date_pretty;
1197 eval qq("$subject");
1202 'Invoice-'. $self->invnum. '.pdf';
1205 =item lpr_data HASHREF
1207 Returns the postscript or plaintext for this invoice as an arrayref.
1209 Options must be passed as a hashref. Positional parameters are no longer
1212 I<template>, if specified, is the name of a suffix for alternate invoices.
1214 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1220 my $conf = $self->conf;
1221 my $opt = shift || {};
1222 if ($opt and !ref($opt)) {
1223 # nobody does this anyway
1224 die "FS::cust_bill::lpr_data called with positional parameters";
1227 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1228 [ $self->$method( $opt ) ];
1233 Prints this invoice.
1235 Options must be passed as a hashref.
1237 I<template>, if specified, is the name of a suffix for alternate invoices.
1239 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1245 return if $self->hide;
1246 my $conf = $self->conf;
1247 my $opt = shift || {};
1248 if ($opt and !ref($opt)) {
1249 die "FS::cust_bill::print called with positional parameters";
1252 my $lpr = delete $opt->{lpr};
1253 if($conf->exists('invoice_print_pdf')) {
1254 # Add the invoice to the current batch.
1255 $self->batch_invoice($opt);
1259 $self->lpr_data($opt),
1260 'agentnum' => $self->cust_main->agentnum,
1266 =item fax_invoice HASHREF
1270 Options must be passed as a hashref.
1272 I<template>, if specified, is the name of a suffix for alternate invoices.
1274 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1280 return if $self->hide;
1281 my $conf = $self->conf;
1282 my $opt = shift || {};
1283 if ($opt and !ref($opt)) {
1284 die "FS::cust_bill::fax_invoice called with positional parameters";
1287 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1288 unless $conf->exists('invoice_latex');
1290 my $dialstring = $self->cust_main->getfield('fax');
1293 my $error = send_fax( 'docdata' => $self->lpr_data($opt),
1294 'dialstring' => $dialstring,
1296 die $error if $error;
1300 =item batch_invoice [ HASHREF ]
1302 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1303 isn't an open batch, one will be created.
1305 HASHREF may contain any options to be passed to C<print_pdf>.
1310 my ($self, $opt) = @_;
1311 my $bill_batch = $self->get_open_bill_batch;
1312 my $cust_bill_batch = FS::cust_bill_batch->new({
1313 batchnum => $bill_batch->batchnum,
1314 invnum => $self->invnum,
1316 if ( $self->mode ) {
1317 $opt->{mode} ||= $self->mode;
1318 $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
1320 return $cust_bill_batch->insert($opt);
1323 =item get_open_batch
1325 Returns the currently open batch as an FS::bill_batch object, creating a new
1326 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1331 sub get_open_bill_batch {
1333 my $conf = $self->conf;
1334 my $hashref = { status => 'O' };
1335 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1336 ? $self->cust_main->agentnum
1338 my $batch = qsearchs('bill_batch', $hashref);
1339 return $batch if $batch;
1340 $batch = FS::bill_batch->new($hashref);
1341 my $error = $batch->insert;
1342 die $error if $error;
1346 =item ftp_invoice [ TEMPLATENAME ]
1348 Sends this invoice data via FTP.
1350 TEMPLATENAME is unused?
1356 my $conf = $self->conf;
1357 my $template = scalar(@_) ? shift : '';
1360 'protocol' => 'ftp',
1361 'server' => $conf->config('cust_bill-ftpserver'),
1362 'username' => $conf->config('cust_bill-ftpusername'),
1363 'password' => $conf->config('cust_bill-ftppassword'),
1364 'dir' => $conf->config('cust_bill-ftpdir'),
1365 'format' => $conf->config('cust_bill-ftpformat'),
1369 =item spool_invoice [ TEMPLATENAME ]
1371 Spools this invoice data (see L<FS::spool_csv>)
1373 TEMPLATENAME is unused?
1379 my $conf = $self->conf;
1380 my $template = scalar(@_) ? shift : '';
1383 'format' => $conf->config('cust_bill-spoolformat'),
1384 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1388 =item send_csv OPTION => VALUE, ...
1390 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1394 protocol - currently only "ftp"
1400 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1401 and YYMMDDHHMMSS is a timestamp.
1403 See L</print_csv> for a description of the output format.
1408 my($self, %opt) = @_;
1412 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1413 mkdir $spooldir, 0700 unless -d $spooldir;
1415 # don't localize dates here, they're a defined format
1416 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1417 my $file = "$spooldir/$tracctnum.csv";
1419 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1421 open(CSV, ">$file") or die "can't open $file: $!";
1429 if ( $opt{protocol} eq 'ftp' ) {
1430 eval "use Net::FTP;";
1432 $net = Net::FTP->new($opt{server}) or die @$;
1434 die "unknown protocol: $opt{protocol}";
1437 $net->login( $opt{username}, $opt{password} )
1438 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1440 $net->binary or die "can't set binary mode";
1442 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1444 $net->put($file) or die "can't put $file: $!";
1454 Spools CSV invoice data.
1460 =item format - any of FS::Misc::::Invoicing::spool_formats
1462 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1463 customer has the corresponding invoice destinations set (see
1464 L<FS::cust_main_invoice>).
1466 =item agent_spools - if set to a true value, will spool to per-agent files
1467 rather than a single global file
1469 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1470 append to that spool. L<FS::Cron::upload> will then send the spool file to
1473 =item balanceover - if set, only spools the invoice if the total amount owed on
1474 this invoice and all older invoices is greater than the specified amount.
1476 =item time - the "current time". Controls the printing of past due messages
1484 my($self, %opt) = @_;
1486 my $time = $opt{'time'} || time;
1487 my $cust_main = $self->cust_main;
1489 if ( $opt{'dest'} ) {
1490 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1491 $cust_main->invoicing_list;
1492 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1493 || ! keys %invoicing_list;
1496 if ( $opt{'balanceover'} ) {
1498 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1501 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1502 mkdir $spooldir, 0700 unless -d $spooldir;
1504 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1507 if ( $opt{'agent_spools'} ) {
1508 $file = 'agentnum'.$cust_main->agentnum;
1513 if ( $opt{'upload_targetnum'} ) {
1514 $spooldir .= '/target'.$opt{'upload_targetnum'};
1515 mkdir $spooldir, 0700 unless -d $spooldir;
1516 } # otherwise it just goes into export.xxx/cust_bill
1518 if ( lc($opt{'format'}) eq 'billco' ) {
1522 $file = "$spooldir/$file.csv";
1524 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1526 open(CSV, ">>$file") or die "can't open $file: $!";
1527 flock(CSV, LOCK_EX);
1532 if ( lc($opt{'format'}) eq 'billco' ) {
1534 flock(CSV, LOCK_UN);
1537 $file =~ s/-header.csv$/-detail.csv/;
1539 open(CSV,">>$file") or die "can't open $file: $!";
1540 flock(CSV, LOCK_EX);
1544 print CSV $detail if defined($detail);
1546 flock(CSV, LOCK_UN);
1553 =item print_csv OPTION => VALUE, ...
1555 Returns CSV data for this invoice.
1559 format - 'default', 'billco', 'oneline', 'bridgestone'
1561 Returns a list consisting of two scalars. The first is a single line of CSV
1562 header information for this invoice. The second is one or more lines of CSV
1563 detail information for this invoice.
1565 If I<format> is not specified or "default", the fields of the CSV file are as
1568 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1569 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1573 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1575 B<record_type> is C<cust_bill> for the initial header line only. The
1576 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1577 fields are filled in.
1579 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1580 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1583 =item invnum - invoice number
1585 =item custnum - customer number
1587 =item _date - invoice date
1589 =item charged - total invoice amount
1591 =item first - customer first name
1593 =item last - customer first name
1595 =item company - company name
1597 =item address1 - address line 1
1599 =item address2 - address line 1
1609 =item pkg - line item description
1611 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1613 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1615 =item sdate - start date for recurring fee
1617 =item edate - end date for recurring fee
1621 If I<format> is "billco", the fields of the header CSV file are as follows:
1623 +-------------------------------------------------------------------+
1624 | FORMAT HEADER FILE |
1625 |-------------------------------------------------------------------|
1626 | Field | Description | Name | Type | Width |
1627 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1628 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1629 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1630 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1631 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1632 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1633 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1634 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1635 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1636 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1637 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1638 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1639 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1640 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1641 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1642 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1643 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1644 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1645 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1646 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1647 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1648 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1649 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1650 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1651 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1652 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1653 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1654 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1655 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1656 +-------+-------------------------------+------------+------+-------+
1658 If I<format> is "billco", the fields of the detail CSV file are as follows:
1660 FORMAT FOR DETAIL FILE
1662 Field | Description | Name | Type | Width
1663 1 | N/A-Leave Empty | RC | CHAR | 2
1664 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1665 3 | Account Number | TRACCTNUM | CHAR | 15
1666 4 | Invoice Number | TRINVOICE | CHAR | 15
1667 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1668 6 | Transaction Detail | DETAILS | CHAR | 100
1669 7 | Amount | AMT | NUM* | 9
1670 8 | Line Format Control** | LNCTRL | CHAR | 2
1671 9 | Grouping Code | GROUP | CHAR | 2
1672 10 | User Defined | ACCT CODE | CHAR | 15
1674 If format is 'oneline', there is no detail file. Each invoice has a
1675 header line only, with the fields:
1677 Agent number, agent name, customer number, first name, last name, address
1678 line 1, address line 2, city, state, zip, invoice date, invoice number,
1679 amount charged, amount due, previous balance, due date.
1681 and then, for each line item, three columns containing the package number,
1682 description, and amount.
1684 If format is 'bridgestone', there is no detail file. Each invoice has a
1685 header line with the following fields in a fixed-width format:
1687 Customer number (in display format), date, name (first last), company,
1688 address 1, address 2, city, state, zip.
1690 This is a mailing list format, and has no per-invoice fields. To avoid
1691 sending redundant notices, the spooling event should have a "once" or
1692 "once_percust_every" condition.
1697 my($self, %opt) = @_;
1699 eval "use Text::CSV_XS";
1702 my $cust_main = $self->cust_main;
1704 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1705 my $format = lc($opt{'format'});
1707 my $time = $opt{'time'} || time;
1709 $self->set('_template', $opt{template})
1710 if exists $opt{template};
1712 my $tracctnum = ''; #leaking out from billco-specific sections :/
1713 if ( $format eq 'billco' ) {
1716 $self->conf->config('billco-account_num', $cust_main->agentnum);
1718 $tracctnum = $account_num eq 'display_custnum'
1719 ? $cust_main->display_custnum
1720 : $opt{'tracctnum'};
1723 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1725 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1727 my( $previous_balance, @unused ) = $self->previous; #previous balance
1729 my $pmt_cr_applied = 0;
1730 $pmt_cr_applied += $_->{'amount'}
1731 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1733 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1736 '', # 1 | N/A-Leave Empty CHAR 2
1737 '', # 2 | N/A-Leave Empty CHAR 15
1738 $tracctnum, # 3 | Transaction Account No CHAR 15
1739 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1740 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1741 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1742 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1743 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1744 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1745 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1746 '', # 10 | Ancillary Billing Information CHAR 30
1747 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1748 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1751 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1754 $duedate, # 14 | Bill Due Date CHAR 10
1756 $previous_balance, # 15 | Previous Balance NUM* 9
1757 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1758 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1759 $totaldue, # 18 | Total Amt Due NUM* 9
1760 $totaldue, # 19 | Total Amt Due NUM* 9
1761 '', # 20 | 30 Day Aging NUM* 9
1762 '', # 21 | 60 Day Aging NUM* 9
1763 '', # 22 | 90 Day Aging NUM* 9
1764 'N', # 23 | Y/N CHAR 1
1765 '', # 24 | Remittance automation CHAR 100
1766 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1767 $self->custnum, # 26 | Customer Reference Number CHAR 15
1768 '0', # 27 | Federal Tax*** NUM* 9
1769 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1770 '0', # 29 | Other Taxes & Fees*** NUM* 9
1773 } elsif ( $format eq 'oneline' ) { #name
1775 my ($previous_balance) = $self->previous;
1776 $previous_balance = sprintf('%.2f', $previous_balance);
1777 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1783 $self->_items_pkg, #_items_nontax? no sections or anything
1788 $cust_main->agentnum,
1789 $cust_main->agent->agent,
1793 $cust_main->company,
1794 $cust_main->address1,
1795 $cust_main->address2,
1801 time2str("%x", $self->_date),
1806 $self->due_date2str("%x"),
1811 } elsif ( $format eq 'bridgestone' ) {
1813 # bypass the CSV stuff and just return this
1814 my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1815 my $zip = $cust_main->zip;
1817 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1821 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1823 $cust_main->display_custnum,
1825 uc(substr($cust_main->contact_firstlast,0,30)),
1826 uc(substr($cust_main->company ,0,30)),
1827 uc(substr($cust_main->address1 ,0,30)),
1828 uc(substr($cust_main->address2 ,0,30)),
1829 uc(substr($cust_main->city ,0,20)),
1830 uc($cust_main->state),
1836 } elsif ( $format eq 'ics' ) {
1838 my $bill = $cust_main->bill_location;
1839 my $zip = $bill->zip;
1843 if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1848 # minor false laziness with print_generic
1849 my ($previous_balance) = $self->previous;
1850 my $balance_due = $self->owed + $previous_balance;
1851 my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1852 my $credit_total = sum(0, map { $_->{'amount'} } $self->_items_credits);
1855 if ( $self->due_date and $time >= $self->due_date ) {
1856 $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1860 my $header = sprintf(
1861 '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1862 $cust_main->display_custnum, #BID
1863 uc($cust_main->first), #FNAME
1864 uc($cust_main->last), #LNAME
1865 '00', #BATCH, should this ever be anything else?
1866 uc($cust_main->company), #COMP
1867 uc($bill->address1), #STREET1
1868 uc($bill->address2), #STREET2
1869 uc($bill->city), #CITY
1870 uc($bill->state), #STATE
1873 time2str('%Y%m%d', $self->_date), #BILL_DATE
1874 $self->due_date2str('%Y%m%d'), #DUE_DATE,
1875 ( map {sprintf('%0.2f', $_)}
1876 $balance_due, #AMNT_DUE
1877 $previous_balance, #PREV_BAL
1878 $payment_total, #PYMT_RCVD
1879 $credit_total, #CREDITS
1880 $previous_balance, #BEG_BAL--is this correct?
1881 $self->charged, #NEW_CHRG
1884 $past_due, #PAST_MSG
1888 my %svc_class = ('' => ''); # maybe cache this more persistently?
1890 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1892 my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1893 my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1897 my @dates = ( $self->_date, undef );
1898 if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1899 $dates[1] = $prev->sdate; #questionable
1902 # generate an 01 detail for each service
1903 my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1904 foreach my $cust_svc ( @svcs ) {
1905 $show_pkgnum = ''; # hide it if we're showing svcnums
1907 my $svcpart = $cust_svc->svcpart;
1908 if (!exists($svc_class{$svcpart})) {
1909 my $classnum = $cust_svc->part_svc->classnum;
1910 my $part_svc_class = FS::part_svc_class->by_key($classnum)
1912 $svc_class{$svcpart} = $part_svc_class ?
1913 $part_svc_class->classname :
1917 my @h_label = $cust_svc->label(@dates, 'I');
1918 push @details, sprintf('01%-9s%-20s%-47s',
1920 $svc_class{$svcpart},
1923 } #foreach $cust_svc
1926 my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1927 if ($cust_bill_pkg->recur > 0) {
1928 $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1929 time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1931 push @details, sprintf('02%-6s%-60s%-10s',
1934 sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1936 } #foreach $cust_bill_pkg
1938 # Tag this row so that we know whether this is one page (1), two pages
1939 # (2), # or "big" (B). The tag will be stripped off before uploading.
1940 if ( scalar(@details) < 12 ) {
1942 } elsif ( scalar(@details) < 58 ) {
1948 return join('', $header, @details, "\n");
1956 time2str("%x", $self->_date),
1957 sprintf("%.2f", $self->charged),
1958 ( map { $cust_main->getfield($_) }
1959 qw( first last company address1 address2 city state zip country ) ),
1961 ) or die "can't create csv";
1964 my $header = $csv->string. "\n";
1967 if ( lc($opt{'format'}) eq 'billco' ) {
1970 my %items_opt = ( format => 'template',
1971 escape_function => sub { shift } );
1972 # I don't know what characters billco actually tolerates in spool entries.
1973 # Text::CSV will take care of delimiters, though.
1975 my @items = ( $self->_items_pkg(%items_opt),
1976 $self->_items_fee(%items_opt) );
1977 foreach my $item (@items) {
1979 my $description = $item->{'description'};
1980 if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1981 $description .= ': ' . $item->{ext_description}[0];
1985 '', # 1 | N/A-Leave Empty CHAR 2
1986 '', # 2 | N/A-Leave Empty CHAR 15
1987 $tracctnum, # 3 | Account Number CHAR 15
1988 $self->invnum, # 4 | Invoice Number CHAR 15
1989 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1990 $description, # 6 | Transaction Detail CHAR 100
1991 $item->{'amount'}, # 7 | Amount NUM* 9
1992 '', # 8 | Line Format Control** CHAR 2
1993 '', # 9 | Grouping Code CHAR 2
1994 '', # 10 | User Defined CHAR 15
1997 $detail .= $csv->string. "\n";
2001 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2007 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2009 my($pkg, $setup, $recur, $sdate, $edate);
2010 if ( $cust_bill_pkg->pkgnum ) {
2012 ($pkg, $setup, $recur, $sdate, $edate) = (
2013 $cust_bill_pkg->part_pkg->pkg,
2014 ( $cust_bill_pkg->setup != 0
2015 ? sprintf("%.2f", $cust_bill_pkg->setup )
2017 ( $cust_bill_pkg->recur != 0
2018 ? sprintf("%.2f", $cust_bill_pkg->recur )
2020 ( $cust_bill_pkg->sdate
2021 ? time2str("%x", $cust_bill_pkg->sdate)
2023 ($cust_bill_pkg->edate
2024 ? time2str("%x", $cust_bill_pkg->edate)
2028 } else { #pkgnum tax
2029 next unless $cust_bill_pkg->setup != 0;
2030 $pkg = $cust_bill_pkg->desc;
2031 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2032 ( $sdate, $edate ) = ( '', '' );
2038 ( map { '' } (1..11) ),
2039 ($pkg, $setup, $recur, $sdate, $edate)
2040 ) or die "can't create csv";
2042 $detail .= $csv->string. "\n";
2048 ( $header, $detail );
2053 croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
2058 Attempts to pay this invoice with a credit card payment via a
2059 Business::OnlinePayment realtime gateway. See
2060 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2061 for supported processors.
2067 $self->realtime_bop( 'CC', @_ );
2072 Attempts to pay this invoice with an electronic check (ACH) payment via a
2073 Business::OnlinePayment realtime gateway. See
2074 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2075 for supported processors.
2081 $self->realtime_bop( 'ECHECK', @_ );
2086 Attempts to pay this invoice with phone bill (LEC) payment via a
2087 Business::OnlinePayment realtime gateway. See
2088 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2089 for supported processors.
2095 $self->realtime_bop( 'LEC', @_ );
2099 my( $self, $method ) = (shift,shift);
2100 my $conf = $self->conf;
2103 my $cust_main = $self->cust_main;
2104 my $balance = $cust_main->balance;
2105 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2106 $amount = sprintf("%.2f", $amount);
2107 return "not run (balance $balance)" unless $amount > 0;
2109 my $description = 'Internet Services';
2110 if ( $conf->exists('business-onlinepayment-description') ) {
2111 my $dtempl = $conf->config('business-onlinepayment-description');
2113 my $agent_obj = $cust_main->agent
2114 or die "can't retreive agent for $cust_main (agentnum ".
2115 $cust_main->agentnum. ")";
2116 my $agent = $agent_obj->agent;
2117 my $pkgs = join(', ',
2118 map { $_->part_pkg->pkg }
2119 grep { $_->pkgnum } $self->cust_bill_pkg
2121 $description = eval qq("$dtempl");
2124 $cust_main->realtime_bop($method, $amount,
2125 'description' => $description,
2126 'invnum' => $self->invnum,
2127 #this didn't do what we want, it just calls apply_payments_and_credits
2129 'apply_to_invoice' => 1,
2132 #this changes application behavior: auto payments
2133 #triggered against a specific invoice are now applied
2134 #to that invoice instead of oldest open.
2140 =item batch_card OPTION => VALUE...
2142 Adds a payment for this invoice to the pending credit card batch (see
2143 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2144 runs the payment using a realtime gateway.
2149 my ($self, %options) = @_;
2150 my $cust_main = $self->cust_main;
2152 $options{invnum} = $self->invnum;
2154 $cust_main->batch_card(%options);
2157 sub _agent_template {
2159 $self->cust_main->agent_template;
2162 sub _agent_invoice_from {
2164 $self->cust_main->agent_invoice_from;
2167 =item invoice_barcode DIR_OR_FALSE
2169 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2170 it is taken as the temp directory where the PNG file will be generated and the
2171 PNG file name is returned. Otherwise, the PNG image itself is returned.
2175 sub invoice_barcode {
2176 my ($self, $dir) = (shift,shift);
2178 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2179 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2180 my $gd = $gdbar->plot(Height => 30);
2183 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2187 ) or die "can't open temp file: $!\n";
2188 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2189 my $png_file = $bh->filename;
2196 =item invnum_date_pretty
2198 Returns a string with the invoice number and date, for example:
2199 "Invoice #54 (3/20/2008)".
2201 Intended for back-end context, with regard to translation and date formatting.
2205 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2206 # for backend use (and also does the wrong thing, localizing for end customer
2207 # instead of backoffice configured date format)
2208 sub invnum_date_pretty {
2210 #$self->mt('Invoice #').
2211 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2212 $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2215 #sub _items_extra_usage_sections {
2217 # my $escape = shift;
2219 # my %sections = ();
2221 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
2222 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2224 # next unless $cust_bill_pkg->pkgnum > 0;
2226 # foreach my $section ( keys %usage_class ) {
2228 # my $usage = $cust_bill_pkg->usage($section);
2230 # next unless $usage && $usage > 0;
2232 # $sections{$section} ||= 0;
2233 # $sections{$section} += $usage;
2239 # map { { 'description' => &{$escape}($_),
2240 # 'subtotal' => $sections{$_},
2241 # 'summarized' => '',
2242 # 'tax_section' => '',
2245 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2249 sub _items_extra_usage_sections {
2251 my $conf = $self->conf;
2259 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2261 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2262 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2263 next unless $cust_bill_pkg->pkgnum > 0;
2265 foreach my $classnum ( keys %usage_class ) {
2266 my $section = $usage_class{$classnum}->classname;
2267 $classnums{$section} = $classnum;
2269 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2270 my $amount = $detail->amount;
2271 next unless $amount && $amount > 0;
2273 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2274 $sections{$section}{amount} += $amount; #subtotal
2275 $sections{$section}{calls}++;
2276 $sections{$section}{duration} += $detail->duration;
2278 my $desc = $detail->regionname;
2279 my $description = $desc;
2280 $description = substr($desc, 0, $maxlength). '...'
2281 if $format eq 'latex' && length($desc) > $maxlength;
2283 $lines{$section}{$desc} ||= {
2284 description => &{$escape}($description),
2285 #pkgpart => $part_pkg->pkgpart,
2286 pkgnum => $cust_bill_pkg->pkgnum,
2291 #unit_amount => $cust_bill_pkg->unitrecur,
2292 quantity => $cust_bill_pkg->quantity,
2293 product_code => 'N/A',
2294 ext_description => [],
2297 $lines{$section}{$desc}{amount} += $amount;
2298 $lines{$section}{$desc}{calls}++;
2299 $lines{$section}{$desc}{duration} += $detail->duration;
2305 my %sectionmap = ();
2306 foreach (keys %sections) {
2307 my $usage_class = $usage_class{$classnums{$_}};
2308 $sectionmap{$_} = { 'description' => &{$escape}($_),
2309 'amount' => $sections{$_}{amount}, #subtotal
2310 'calls' => $sections{$_}{calls},
2311 'duration' => $sections{$_}{duration},
2313 'tax_section' => '',
2314 'sort_weight' => $usage_class->weight,
2315 ( $usage_class->format
2316 ? ( map { $_ => $usage_class->$_($format) }
2317 qw( description_generator header_generator total_generator total_line_generator )
2324 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2328 foreach my $section ( keys %lines ) {
2329 foreach my $line ( keys %{$lines{$section}} ) {
2330 my $l = $lines{$section}{$line};
2331 $l->{section} = $sectionmap{$section};
2332 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2333 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2338 return(\@sections, \@lines);
2344 my $end = $self->_date;
2346 # start at date of previous invoice + 1 second or 0 if no previous invoice
2347 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2348 $start = 0 if !$start;
2351 my $cust_main = $self->cust_main;
2352 my @pkgs = $cust_main->all_pkgs;
2353 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2356 foreach my $pkg ( @pkgs ) {
2357 my @h_cust_svc = $pkg->h_cust_svc($end);
2358 foreach my $h_cust_svc ( @h_cust_svc ) {
2359 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2360 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2362 my $inserted = $h_cust_svc->date_inserted;
2363 my $deleted = $h_cust_svc->date_deleted;
2364 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2366 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
2368 # DID either activated or ported in; cannot be both for same DID simultaneously
2369 if ($inserted >= $start && $inserted <= $end && $phone_inserted
2370 && (!$phone_inserted->lnp_status
2371 || $phone_inserted->lnp_status eq ''
2372 || $phone_inserted->lnp_status eq 'native')) {
2375 else { # this one not so clean, should probably move to (h_)svc_phone
2376 local($FS::Record::qsearch_qualify_columns) = 0;
2377 my $phone_portedin = qsearchs( 'h_svc_phone',
2378 { 'svcnum' => $h_cust_svc->svcnum,
2379 'lnp_status' => 'portedin' },
2380 FS::h_svc_phone->sql_h_searchs($end),
2382 $num_portedin++ if $phone_portedin;
2385 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2386 if($deleted >= $start && $deleted <= $end && $phone_deleted
2387 && (!$phone_deleted->lnp_status
2388 || $phone_deleted->lnp_status ne 'portingout')) {
2391 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2392 && $phone_deleted->lnp_status
2393 && $phone_deleted->lnp_status eq 'portingout') {
2397 # increment usage minutes
2398 if ( $phone_inserted ) {
2399 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2400 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2403 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2406 # don't look at this service again
2407 push @seen, $h_cust_svc->svcnum;
2411 $minutes = sprintf("%d", $minutes);
2412 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
2413 . "$num_deactivated Ported-Out: $num_portedout ",
2414 "Total Minutes: $minutes");
2417 sub _items_accountcode_cdr {
2422 my $section = { 'amount' => 0,
2425 'sort_weight' => '',
2427 'description' => 'Usage by Account Code',
2433 my %accountcodes = ();
2435 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2436 next unless $cust_bill_pkg->pkgnum > 0;
2438 my @header = $cust_bill_pkg->details_header;
2439 next unless scalar(@header);
2440 $section->{'header'} = join(',',@header);
2442 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2444 $section->{'header'} = $detail->formatted('format' => $format)
2445 if($detail->detail eq $section->{'header'});
2447 my $accountcode = $detail->accountcode;
2448 next unless $accountcode;
2450 my $amount = $detail->amount;
2451 next unless $amount && $amount > 0;
2453 $accountcodes{$accountcode} ||= {
2454 description => $accountcode,
2461 product_code => 'N/A',
2462 section => $section,
2463 ext_description => [ $section->{'header'} ],
2467 $section->{'amount'} += $amount;
2468 $accountcodes{$accountcode}{'amount'} += $amount;
2469 $accountcodes{$accountcode}{calls}++;
2470 $accountcodes{$accountcode}{duration} += $detail->duration;
2471 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2475 foreach my $l ( values %accountcodes ) {
2476 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2477 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2478 foreach my $sorted_detail ( @sorted_detail ) {
2479 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2481 delete $l->{detail_temp};
2485 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2487 return ($section,\@sorted_lines);
2490 sub _items_svc_phone_sections {
2492 my $conf = $self->conf;
2500 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2502 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2503 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2505 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2506 next unless $cust_bill_pkg->pkgnum > 0;
2508 my @header = $cust_bill_pkg->details_header;
2509 next unless scalar(@header);
2511 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2513 my $phonenum = $detail->phonenum;
2514 next unless $phonenum;
2516 my $amount = $detail->amount;
2517 next unless $amount && $amount > 0;
2519 $sections{$phonenum} ||= { 'amount' => 0,
2522 'sort_weight' => -1,
2523 'phonenum' => $phonenum,
2525 $sections{$phonenum}{amount} += $amount; #subtotal
2526 $sections{$phonenum}{calls}++;
2527 $sections{$phonenum}{duration} += $detail->duration;
2529 my $desc = $detail->regionname;
2530 my $description = $desc;
2531 $description = substr($desc, 0, $maxlength). '...'
2532 if $format eq 'latex' && length($desc) > $maxlength;
2534 $lines{$phonenum}{$desc} ||= {
2535 description => &{$escape}($description),
2536 #pkgpart => $part_pkg->pkgpart,
2544 product_code => 'N/A',
2545 ext_description => [],
2548 $lines{$phonenum}{$desc}{amount} += $amount;
2549 $lines{$phonenum}{$desc}{calls}++;
2550 $lines{$phonenum}{$desc}{duration} += $detail->duration;
2552 my $line = $usage_class{$detail->classnum}->classname;
2553 $sections{"$phonenum $line"} ||=
2557 'sort_weight' => $usage_class{$detail->classnum}->weight,
2558 'phonenum' => $phonenum,
2559 'header' => [ @header ],
2561 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
2562 $sections{"$phonenum $line"}{calls}++;
2563 $sections{"$phonenum $line"}{duration} += $detail->duration;
2565 $lines{"$phonenum $line"}{$desc} ||= {
2566 description => &{$escape}($description),
2567 #pkgpart => $part_pkg->pkgpart,
2575 product_code => 'N/A',
2576 ext_description => [],
2579 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2580 $lines{"$phonenum $line"}{$desc}{calls}++;
2581 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2582 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2583 $detail->formatted('format' => $format);
2588 my %sectionmap = ();
2589 my $simple = new FS::usage_class { format => 'simple' }; #bleh
2590 foreach ( keys %sections ) {
2591 my @header = @{ $sections{$_}{header} || [] };
2593 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2594 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2595 my $usage_class = $summary ? $simple : $usage_simple;
2596 my $ending = $summary ? ' usage charges' : '';
2599 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2601 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2602 'amount' => $sections{$_}{amount}, #subtotal
2603 'calls' => $sections{$_}{calls},
2604 'duration' => $sections{$_}{duration},
2606 'tax_section' => '',
2607 'phonenum' => $sections{$_}{phonenum},
2608 'sort_weight' => $sections{$_}{sort_weight},
2609 'post_total' => $summary, #inspire pagebreak
2611 ( map { $_ => $usage_class->$_($format, %gen_opt) }
2612 qw( description_generator
2615 total_line_generator
2622 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2623 $a->{sort_weight} <=> $b->{sort_weight}
2628 foreach my $section ( keys %lines ) {
2629 foreach my $line ( keys %{$lines{$section}} ) {
2630 my $l = $lines{$section}{$line};
2631 $l->{section} = $sectionmap{$section};
2632 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2633 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2638 if($conf->exists('phone_usage_class_summary')) {
2639 # this only works with Latex
2643 # after this, we'll have only two sections per DID:
2644 # Calls Summary and Calls Detail
2645 foreach my $section ( @sections ) {
2646 if($section->{'post_total'}) {
2647 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2648 $section->{'total_line_generator'} = sub { '' };
2649 $section->{'total_generator'} = sub { '' };
2650 $section->{'header_generator'} = sub { '' };
2651 $section->{'description_generator'} = '';
2652 push @newsections, $section;
2653 my %calls_detail = %$section;
2654 $calls_detail{'post_total'} = '';
2655 $calls_detail{'sort_weight'} = '';
2656 $calls_detail{'description_generator'} = sub { '' };
2657 $calls_detail{'header_generator'} = sub {
2658 return ' & Date/Time & Called Number & Duration & Price'
2659 if $format eq 'latex';
2662 $calls_detail{'description'} = 'Calls Detail: '
2663 . $section->{'phonenum'};
2664 push @newsections, \%calls_detail;
2668 # after this, each usage class is collapsed/summarized into a single
2669 # line under the Calls Summary section
2670 foreach my $newsection ( @newsections ) {
2671 if($newsection->{'post_total'}) { # this means Calls Summary
2672 foreach my $section ( @sections ) {
2673 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
2674 && !$section->{'post_total'});
2675 my $newdesc = $section->{'description'};
2676 my $tn = $section->{'phonenum'};
2677 $newdesc =~ s/$tn//g;
2678 my $line = { ext_description => [],
2682 calls => $section->{'calls'},
2683 section => $newsection,
2684 duration => $section->{'duration'},
2685 description => $newdesc,
2686 amount => sprintf("%.2f",$section->{'amount'}),
2687 product_code => 'N/A',
2689 push @newlines, $line;
2694 # after this, Calls Details is populated with all CDRs
2695 foreach my $newsection ( @newsections ) {
2696 if(!$newsection->{'post_total'}) { # this means Calls Details
2697 foreach my $line ( @lines ) {
2698 next unless (scalar(@{$line->{'ext_description'}}) &&
2699 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2701 my @extdesc = @{$line->{'ext_description'}};
2703 foreach my $extdesc ( @extdesc ) {
2704 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2705 push @newextdesc, $extdesc;
2707 $line->{'ext_description'} = \@newextdesc;
2708 $line->{'section'} = $newsection;
2709 push @newlines, $line;
2714 return(\@newsections, \@newlines);
2717 return(\@sections, \@lines);
2721 =sub _items_usage_class_summary OPTIONS
2723 Returns a list of detail items summarizing the usage charges on this
2724 invoice. Each one will have 'amount', 'description' (the usage charge name),
2725 and 'usage_classnum'.
2727 OPTIONS can include 'escape' (a function to escape the descriptions).
2731 sub _items_usage_class_summary {
2735 my $escape = $opt{escape} || sub { $_[0] };
2736 my $money_char = $opt{money_char};
2737 my $invnum = $self->invnum;
2738 my @classes = qsearch({
2739 'table' => 'usage_class',
2740 'select' => 'classnum, classname, SUM(amount) AS amount,'.
2741 ' COUNT(*) AS calls, SUM(duration) AS duration',
2742 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2743 ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2744 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2745 ' GROUP BY classnum, classname, weight'.
2746 ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2747 ' ORDER BY weight ASC',
2751 description => &{$escape}($self->mt('Usage Summary')),
2755 foreach my $class (@classes) {
2756 $section->{subtotal} += $class->get('amount');
2758 'description' => &{$escape}($class->classname),
2759 'amount' => $money_char.sprintf('%.2f', $class->get('amount')),
2760 'quantity' => $class->get('calls'),
2761 'duration' => $class->get('duration'),
2762 'usage_classnum' => $class->classnum,
2763 'section' => $section,
2766 $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
2770 =sub _items_previous()
2772 Returns an array of hashrefs, each hashref representing a line-item on
2773 the current bill for previous unpaid invoices.
2775 keys for each previous_item:
2776 - amount (see notes)
2782 Payments and credits shown on this invoice may vary based on configuraiton.
2784 when conf flag previous_balance-payments_since is set:
2785 This method works backwards to rebuild the invoice as a snapshot in time.
2786 The invoice displayed will have the balances owed, and payments made,
2787 reflecting the state of the account at the time of invoice generation.
2791 sub _items_previous {
2796 if ($self->get('_items_previous')) {
2797 return sort { $a->{_date} <=> $b->{_date} }
2798 values %{ $self->get('_items_previous') };
2801 # Gets the customer's current balance and outstanding invoices.
2802 my ($prev_balance, @open_invoices) = $self->previous;
2804 my %invoices = map {
2805 $_->invnum => $self->__items_previous_map_invoice($_)
2808 # Which credits and payments displayed on the bill will vary based on
2809 # conf flag previous_balance-payments_since.
2810 my @credits = $self->_items_credits();
2811 my @payments = $self->_items_payments();
2814 if ($self->conf->exists('previous_balance-payments_since')) {
2815 # For each credit or payment, determine which invoices it was applied to.
2816 # Manipulate data displayed so the invoice displayed appears as a
2817 # snapshot in time... with previous balances and balance owed displayed
2818 # as they were at the time of invoice creation.
2820 my @credits_postbill = $self->_items_credits_postbill();
2821 my @payments_postbill = $self->_items_payments_postbill();
2826 # Each section below follows this pattern on a payment/credit
2828 # - Dupe check, avoid adjusting for the same item twice
2829 # - If invoice being adjusted for isn't in our list, add it
2830 # - Adjust the invoice balance to refelct balnace without the
2831 # credit or payment applied
2834 # Working with payments displayed on this bill
2835 for my $pmt_hash (@payments) {
2836 my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
2837 for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
2838 next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
2839 $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
2841 my $invnum = $cust_bill_pay->invnum;
2843 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2844 unless exists $invoices{$invnum};
2846 $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
2850 # Working with credits displayed on this bill
2851 for my $cred_hash (@credits) {
2852 my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
2853 for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
2854 next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
2855 $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
2857 my $invnum = $cust_credit_bill->invnum;
2859 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2860 unless exists $invoices{$invnum};
2862 $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
2866 # Working with both credits and payments which are not displayed
2867 # on this bill, but which have affected this bill's balances
2868 for my $postbill (@payments_postbill, @credits_postbill) {
2870 if ($postbill->{billpaynum}) {
2871 next if exists $pmnt_dupechk{$postbill->{billpaynum}};
2872 $pmnt_dupechk{$postbill->{billpaynum}} = 1;
2873 } elsif ($postbill->{creditbillnum}) {
2874 next if exists $cred_dupechk{$postbill->{creditbillnum}};
2875 $cred_dupechk{$postbill->{creditbillnum}} = 1;
2877 die "Missing creditbillnum or billpaynum";
2880 my $invnum = $postbill->{invnum};
2882 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2883 unless exists $invoices{$invnum};
2885 $invoices{$invnum}->{amount} += $postbill->{amount};
2888 # Make sure current invoice doesn't appear in previous items
2889 delete $invoices{$self->invnum}
2890 if exists $invoices{$self->invnum};
2894 # Make sure amount is formatted as a dollar string
2895 # (Formatting should happen on the template side, but is not?)
2896 $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
2899 $self->set('_items_previous', \%invoices);
2900 return sort { $a->{_date} <=> $b->{_date} } values %invoices;
2904 =sub _items_previous_total
2906 Return sum of amounts from all items returned by _items_previous
2907 Results will vary based on invoicing conf flags
2911 sub _items_previous_total {
2914 $tot += $_->{amount} for $self->_items_previous();
2918 sub __items_previous_get_invoice {
2919 # Helper function for _items_previous
2921 # Read a record from cust_bill, return a hash of it's information
2922 my ($self, $invnum) = @_;
2923 die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
2925 my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
2926 return $self->__items_previous_map_invoice($cust_bill);
2929 sub __items_previous_map_invoice {
2930 # Helper function for _items_previous
2932 # Transform a cust_bill object into a simple hash reference of the type
2933 # required by _items_previous
2934 my ($self, $cust_bill) = @_;
2935 die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
2937 my $date = $self->conf->exists('invoice_show_prior_due_date')
2938 ? 'due '.$cust_bill->due_date2str('short')
2939 : $self->time2str_local('short', $cust_bill->_date);
2942 invnum => $cust_bill->invnum,
2943 amount => $cust_bill->owed,
2945 _date => $cust_bill->_date,
2946 description => join(' ',
2947 $self->mt('Previous Balance, Invoice #'),
2954 =sub _items_credits()
2956 Return array of hashrefs containing credits to be shown as line-items
2957 when rendering this bill.
2959 keys for each credit item:
2960 - crednum: id of payment
2961 - amount: payment amount
2962 - description: line item to be displayed on the bill
2964 This method has three ways it selects which credits to display on
2967 1) Default Case: No Conf flag for 'previous_balance-payments_since'
2969 Returns credits that have been applied to this bill only
2972 Conf flag set for 'previous_balance-payments_since'
2974 List all credits that have been recorded during the time period
2975 between the timestamps of the last invoice and this invoice
2978 Conf flag set for 'previous_balance-payments_since'
2979 $opt{'template'} eq 'statement'
2981 List all payments that have been recorded between the timestamps
2982 of the previous invoice and the following invoice.
2984 This is used to give the customer a receipt for a payment
2985 in the form of their last bill with the payment amended.
2987 I am concerned with this implementation, but leaving in place as is
2988 If this option is selected, while viewing an older bill, the old bill
2989 will show ALL future credits for future bills, but no charges for
2990 future bills. Somebody could be misled into believing they have a
2991 large account credit when they don't. Also, interrupts the chain of
2992 invoices as an account history... the customer could have two invoices
2993 in their fileing cabinet, for two different dates, both with a line item
2994 for the same duplicate credit. The accounting is technically accurate,
2995 but somebody could easily become confused and think two credits were
2996 made, when really those two line items on two different bills represent
2997 only a single credit
3001 sub _items_credits {
3006 return @{$self->get('_items_credits')} if $self->get('_items_credits');
3009 my $template = $opt{template} || $self->get('_template');
3010 my $trim_len = $opt{template} || $self->get('trim_len') || 40;
3013 my @cust_credit_objs;
3015 if ($self->conf->exists('previous_balance-payments_since')) {
3016 if ($template eq 'statement') {
3017 # Case 3 (see above)
3018 # Return credits timestamped between the previous and following bills
3020 my $previous_bill = $self->previous_bill;
3021 my $following_bill = $self->following_bill;
3023 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3024 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3027 table => 'cust_credit',
3029 custnum => $self->custnum,
3030 _date => { op => '>=', value => $date_start },
3033 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3035 @cust_credit_objs = qsearch(\%query);
3038 # Case 2 (see above)
3039 # Return credits timestamps between this and the previous bills
3042 my $date_end = $self->_date;
3044 my $previous_bill = $self->previous_bill;
3045 if (ref $previous_bill) {
3046 $date_start = $previous_bill->_date;
3049 @cust_credit_objs = qsearch({
3050 table => 'cust_credit',
3052 custnum => $self->custnum,
3053 _date => {op => '>=', value => $date_start},
3055 extra_sql => " AND _date <= $date_end ",
3060 # Case 1 (see above)
3061 # Return only credits that have been applied to this bill
3063 @cust_credit_objs = $self->cust_credited;
3067 # Translate objects into hashrefs
3068 foreach my $obj ( @cust_credit_objs ) {
3069 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3071 amount => sprintf('%.2f',$cust_credit->amount),
3072 crednum => $cust_credit->crednum,
3073 _date => $cust_credit->_date,
3074 creditreason => $cust_credit->reason,
3077 my $reason = substr($cust_credit->reason, 0, $trim_len);
3078 $reason .= '...' if length($reason) < length($cust_credit->reason);
3079 $reason = "($reason)" if $reason;
3081 $r_obj{description} = join(' ',
3082 $self->mt('Credit applied'),
3083 $self->time2str_local('short', $cust_credit->_date),
3087 push @return, \%r_obj;
3089 $self->set('_items_credits',\@return);
3093 =sub _items_credits_total
3095 Return the total of al items from _items_credits
3096 Will vary based on invoice display conf flag
3100 sub _items_credits_total {
3103 $tot += $_->{amount} for $self->_items_credits();
3109 =sub _items_credits_postbill()
3111 Returns an array of hashrefs for credits where
3112 - Credit issued after this invoice
3113 - Credit applied to an invoice before this invoice
3115 Returned hashrefs are of the format returned by _items_credits()
3119 sub _items_credits_postbill {
3122 my @cust_credit_bill = qsearch({
3123 table => 'cust_credit_bill',
3124 select => join(', ',qw(
3125 cust_credit_bill.creditbillnum
3126 cust_credit_bill._date
3127 cust_credit_bill.invnum
3128 cust_credit_bill.amount
3130 addl_from => ' LEFT JOIN cust_credit'.
3131 ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
3132 extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum.
3133 ' AND cust_credit_bill._date > '.$self->_date.
3134 ' AND cust_credit_bill.invnum < '.$self->invnum.' ',
3135 #! did not investigate why hashref doesn't work for this join query
3137 # 'cust_credit.custnum' => {op => '=', value => $self->custnum},
3138 # 'cust_credit_bill._date' => {op => '>', value => $self->_date},
3139 # 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
3145 invnum => $_->invnum,
3146 amount => $_->amount,
3147 creditbillnum => $_->creditbillnum,
3148 }} @cust_credit_bill;
3151 =sub _items_payments_postbill()
3153 Returns an array of hashrefs for payments where
3154 - Payment occured after this invoice
3155 - Payment applied to an invoice before this invoice
3157 Returned hashrefs are of the format returned by _items_payments()
3161 sub _items_payments_postbill {
3164 my @cust_bill_pay = qsearch({
3165 table => 'cust_bill_pay',
3166 select => join(', ',qw(
3167 cust_bill_pay.billpaynum
3169 cust_bill_pay.invnum
3170 cust_bill_pay.amount
3172 addl_from => ' LEFT JOIN cust_bill'.
3173 ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
3174 extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum.
3175 ' AND cust_bill_pay._date > '.$self->_date.
3176 ' AND cust_bill_pay.invnum < '.$self->invnum.' ',
3181 invnum => $_->invnum,
3182 amount => $_->amount,
3183 billpaynum => $_->billpaynum,
3187 =sub _items_payments()
3189 Return array of hashrefs containing payments to be shown as line-items
3190 when rendering this bill.
3192 keys for each payment item:
3193 - paynum: id of payment
3194 - amount: payment amount
3195 - description: line item to be displayed on the bill
3197 This method has three ways it selects which payments to display on
3200 1) Default Case: No Conf flag for 'previous_balance-payments_since'
3202 Returns payments that have been applied to this bill only
3205 Conf flag set for 'previous_balance-payments_since'
3207 List all payments that have been recorded between the timestamps
3208 of the previous invoice and this invoice
3211 Conf flag set for 'previous_balance-payments_since'
3212 $opt{'template'} eq 'statement'
3214 List all payments that have been recorded between the timestamps
3215 of the previous invoice and the following invoice.
3217 I am concerned with this implementation, but leaving in place as is
3218 If this option is selected, while viewing an older bill, the old bill
3219 will show ALL future payments for future bills, but no charges for
3220 future bills. Somebody could be misled into believing they have a
3221 large account credit when they don't. Also, interrupts the chain of
3222 invoices as an account history... the customer could have two invoices
3223 in their fileing cabinet, for two different dates, both with a line item
3224 for the same duplicate payment. The accounting is technically accurate,
3225 but somebody could easily become confused and think two payments were
3226 made, when really those two line items on two different bills represent
3227 only a single payment.
3231 sub _items_payments {
3236 return @{$self->get('_items_payments')} if $self->get('_items_payments');
3239 my $template = $opt{template} || $self->get('_template');
3244 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3246 if ($self->conf->exists('previous_balance-payments_since')) {
3247 if ($template eq 'statement') {
3249 # Case 3 (see above)
3250 # Return payments timestamped between the previous and following bills
3252 my $previous_bill = $self->previous_bill;
3253 my $following_bill = $self->following_bill;
3255 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3256 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3259 table => 'cust_pay',
3261 custnum => $self->custnum,
3262 _date => { op => '>=', value => $date_start },
3265 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3267 @cust_pay_objs = qsearch(\%query);
3270 # Case 2 (see above)
3271 # Return payments timestamped between this and the previous bill
3274 my $date_end = $self->_date;
3276 my $previous_bill = $self->previous_bill;
3277 if (ref $previous_bill) {
3278 $date_start = $previous_bill->_date;
3281 @cust_pay_objs = qsearch({
3282 table => 'cust_pay',
3284 custnum => $self->custnum,
3285 _date => {op => '>=', value => $date_start},
3287 extra_sql => " AND _date <= $date_end ",
3292 # Case 1 (see above)
3293 # Return payments applied only to this bill
3295 @cust_pay_objs = $self->cust_bill_pay;
3301 [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
3303 return @{ $self->get('_items_payments') };
3306 =sub _items_payments_total
3308 Return a total of all records returned by _items_payments
3309 Results vary based on invoicing conf flags
3313 sub _items_payments_total {
3316 $tot += $_->{amount} for $self->_items_payments();
3320 sub __items_payments_make_hashref {
3321 # Transform a FS::cust_pay object into a simple hashref for invoice
3322 my ($self, @cust_pay_objs) = @_;
3323 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3326 for my $obj (@cust_pay_objs) {
3328 # In case we're passed FS::cust_bill_pay (or something else?)
3329 # Below, we use $obj to render amount rather than $cust_apy.
3330 # If we were passed cust_bill_pay objs, then:
3331 # $obj->amount represents the amount applied to THIS invoice
3332 # $cust_pay->amount represents the total payment, which may have
3333 # been applied accross several invoices.
3334 # If we were passed cust_bill_pay objects, then the conf flag
3335 # previous_balance-payments_since is NOT set, so we should not
3336 # present any payments not applied to this invoice.
3337 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3340 _date => $cust_pay->_date,
3341 amount => sprintf("%.2f", $obj->amount),
3342 paynum => $cust_pay->paynum,
3343 payinfo => $cust_pay->payby_payinfo_pretty(),
3344 description => join(' ',
3345 $self->mt('Payment received'),
3346 $self->time2str_local('short', $cust_pay->_date),
3350 if ($c_invoice_payment_details) {
3351 $r_obj{description} = join(' ',
3352 $r_obj{description},
3354 $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
3358 push @return, \%r_obj;
3365 Generate the line-items to be shown on the bill in the "Totals" section
3367 Returns a list of hashrefs, each with the keys:
3368 - total_item: description field
3369 - total_amount: dollar-formatted number amount
3371 Information presented by this method varies based on Conf
3373 Conf previous_balance-payments_due
3374 - default, flag not set
3375 Only transactions that were applied to this bill bill be
3376 displayed and calculated intothe total. If items exist in
3377 the past-due section, those items will disappear from this
3378 invoice if they have been paid off.
3380 - previous_balance-payments_due flag is set
3381 Transactions occuring after the timestsamp of this
3382 invoice are not reflected on invoice line items
3384 Only payments/credits applied between the previous invoice
3385 and this one are displayed and calculated into the total
3387 - previous_balance-payments_due && $opt{template} eq 'statement'
3388 Same as above, except payments/credits occuring before the date
3389 of the following invoice are also displayed and calculated into
3392 Conf previous_balance-exclude_from_total
3393 - default, flag not set
3394 The "Totals" section contains a single line item.
3395 The dollar amount of this line items is a sum of old and new charges
3396 - previous_balance-exclude_from_total flag is set
3397 The "Totals" section contains two line items.
3398 One for previous balance, one for new charges
3399 !NOTE: Avent virtualization flag 'disable_previous_balance' can
3400 override the global conf flag previous_balance-exclude_from_total
3402 Conf invoice_show_prior_due_date
3403 - default, flag not set
3404 Total line item in the "Totals" section does not mention due date
3405 - invoice_show_prior_due_date flag is set
3406 Total line item in the "Totals" section includes either the due
3407 date of the invoice, or the specified invoice terms
3408 ? Not sure why this is called "Prior" due date, since we seem to be
3409 displaying THIS due date...
3414 my $conf = $self->conf;
3416 my $c_multi_line_total = 0;
3417 $c_multi_line_total = 1
3418 if $conf->exists('previous_balance-exclude_from_total')
3419 && $self->enable_previous();
3422 my $invoice_charges = $self->charged();
3424 # _items_previous() is aware of conf flags
3425 my $previous_balance = 0;
3426 $previous_balance += $_->{amount} for $self->_items_previous();
3431 if ( $previous_balance && $c_multi_line_total ) {
3432 # previous balance, new charges on separate lines
3435 total_amount => sprintf('%.2f',$previous_balance),
3436 total_item => $self->mt(
3437 $conf->config('previous_balance-text') || 'Previous Balance'
3441 $total_charges = $invoice_charges;
3442 $total_descr = $self->mt(
3443 $conf->config('previous_balance-text-total_new_charges')
3444 || 'Total New Charges'
3448 # previous balance and new charges combined into a single total line
3449 $total_charges = $invoice_charges + $previous_balance;
3450 $total_descr = $self->mt('Total Charges');
3453 if ( $conf->exists('invoice_show_prior_due_date') ) {
3454 # then the due date should be shown with Total New Charges,
3455 # and should NOT be shown with the Balance Due message.
3457 if ( $self->due_date ) {
3458 $total_descr = join(' ',
3461 $self->mt('Please pay by'),
3462 $self->due_date2str('short')
3464 } elsif ( $self->terms ) {
3465 $total_descr = join(' ',
3468 $self->mt($self->terms)
3474 total_amount => sprintf('%.2f', $total_charges),
3475 total_item => $total_descr,
3481 =item _items_aging_balances
3483 Returns an array of aged balance amounts from a given epoch timestamp.
3485 The time of day is ignored for this calculation, so that slight differences
3486 on the generation time of an invoice doesn't determine which column an
3487 aged balance falls into.
3489 Will not include any balances dated after the given timestamp in
3490 the calculated totals
3493 @aged_balances = $b->_items_aging_balances( $b->_date )
3504 sub _items_aging_balances {
3505 my ($self, $basetime) = @_;
3506 die "Incorrect usage of _items_aging_balances()" unless ref $self;
3508 $basetime = $self->_date unless $basetime;
3509 my @aging_balances = (0, 0, 0, 0);
3510 my @open_invoices = $self->_items_previous();
3511 my $d30 = 2592000; # 60 * 60 * 24 * 30,
3512 my $d60 = 5184000; # 60 * 60 * 24 * 60,
3513 my $d90 = 7776000; # 60 * 60 * 24 * 90
3515 # Move the clock back on our given day to 12:00:01 AM
3516 my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
3517 my $dt_12am = DateTime->new(
3518 year => $dt_basetime->year,
3519 month => $dt_basetime->month,
3520 day => $dt_basetime->day,
3526 # set our epoch breakpoints
3527 $_ = $dt_12am - $_ for $d30, $d60, $d90;
3529 # grep the aged balances
3530 for my $oinv (@open_invoices) {
3531 if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
3532 # If post invoice dated less than 30days ago
3533 $aging_balances[0] += $oinv->{amount};
3534 } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
3535 # If past invoice dated between 30-60 days ago
3536 $aging_balances[1] += $oinv->{amount};
3537 } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
3538 # If past invoice dated between 60-90 days ago
3539 $aging_balances[2] += $oinv->{amount};
3541 # If past invoice dated 90+ days ago
3542 $aging_balances[3] += $oinv->{amount};
3546 return map{ sprintf('%.2f',$_) } @aging_balances;
3549 =item call_details [ OPTION => VALUE ... ]
3551 Returns an array of CSV strings representing the call details for this invoice
3552 The only option available is the boolean prepend_billed_number
3557 my ($self, %opt) = @_;
3559 my $format_function = sub { shift };
3561 if ($opt{prepend_billed_number}) {
3562 $format_function = sub {
3566 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3571 my @details = map { $_->details( 'format_function' => $format_function,
3572 'escape_function' => sub{ return() },
3576 $self->cust_bill_pkg;
3577 my $header = $details[0];
3578 ( $header, grep { $_ ne $header } @details );
3581 =item cust_pay_batch
3583 Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
3588 sub cust_pay_batch {
3589 carp "FS::cust_bill->cust_pay_batch is deprecated";
3591 qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
3600 =item process_reprint
3604 sub process_reprint {
3605 process_re_X('print', @_);
3608 =item process_reemail
3612 sub process_reemail {
3613 process_re_X('email', @_);
3621 process_re_X('fax', @_);
3629 process_re_X('ftp', @_);
3636 sub process_respool {
3637 process_re_X('spool', @_);
3642 my( $method, $job ) = ( shift, shift );
3643 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3646 warn Dumper($param) if $DEBUG;
3656 # this is called from search/cust_bill.html and given all its search
3657 # parameters, so it needs to perform the same search.
3660 # spool_invoice ftp_invoice fax_invoice print_invoice
3661 my($method, $job, %param ) = @_;
3663 warn "re_X $method for job $job with param:\n".
3664 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3667 #some false laziness w/search/cust_bill.html
3668 $param{'order_by'} = 'cust_bill._date';
3670 my $query = FS::cust_bill->search(\%param);
3671 delete $query->{'count_query'};
3672 delete $query->{'count_addl'};
3674 $query->{debug} = 1; # was in here before, is obviously useful
3676 my @cust_bill = qsearch( $query );
3678 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3680 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3683 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3684 foreach my $cust_bill ( @cust_bill ) {
3685 $cust_bill->$method();
3687 if ( $job ) { #progressbar foo
3689 if ( time - $min_sec > $last ) {
3690 my $error = $job->update_statustext(
3691 int( 100 * $num / scalar(@cust_bill) )
3693 die $error if $error;
3704 +{ ( map { $_=>$self->$_ } $self->fields ),
3705 'owed' => $self->owed,
3706 #XXX last payment applied date
3712 =head1 CLASS METHODS
3718 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3723 my ($class, $start, $end) = @_;
3725 $class->paid_sql($start, $end). ' - '.
3726 $class->credited_sql($start, $end);
3731 Returns an SQL fragment to retreive the net amount (charged minus credited).
3736 my ($class, $start, $end) = @_;
3737 'charged - '. $class->credited_sql($start, $end);
3742 Returns an SQL fragment to retreive the amount paid against this invoice.
3747 my ($class, $start, $end) = @_;
3748 $start &&= "AND cust_bill_pay._date <= $start";
3749 $end &&= "AND cust_bill_pay._date > $end";
3750 $start = '' unless defined($start);
3751 $end = '' unless defined($end);
3752 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3753 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
3758 Returns an SQL fragment to retreive the amount credited against this invoice.
3763 my ($class, $start, $end) = @_;
3764 $start &&= "AND cust_credit_bill._date <= $start";
3765 $end &&= "AND cust_credit_bill._date > $end";
3766 $start = '' unless defined($start);
3767 $end = '' unless defined($end);
3768 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3769 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
3774 Returns an SQL fragment to retrieve the due date of an invoice.
3775 Currently only supported on PostgreSQL.
3780 die "don't use: doesn't account for agent-specific invoice_default_terms";
3782 #we're passed a $conf but not a specific customer (that's in the query), so
3783 # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3784 # "condition_sql_option" that retreives a conf value with SQL in an agent-
3787 my $conf = new FS::Conf;
3791 cust_bill.invoice_terms,
3792 cust_main.invoice_terms,
3793 \''.($conf->config('invoice_default_terms') || '').'\'
3794 ), E\'Net (\\\\d+)\'
3796 ) * 86400 + cust_bill._date'
3807 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3808 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base