package FS::cust_bill;
-use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
+use base qw( FS::cust_bill::Search FS::Template_Mixin
+ FS::cust_main_Mixin FS::Record
+ );
use strict;
-use vars qw( $DEBUG $me $date_format );
+use vars qw( $DEBUG $me );
# but NOT $conf
+use Carp;
use Fcntl qw(:flock); #for spool_csv
use Cwd;
-use List::Util qw(min max);
+use List::Util qw(min max sum);
use Date::Format;
+use DateTime;
use File::Temp 0.14;
use HTML::Entities;
use Storable qw( freeze thaw );
use GD::Barcode;
use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax do_print );
+use FS::Misc qw( send_fax do_print );
use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main;
use FS::cust_statement;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
use FS::cust_pkg;
use FS::cust_credit_bill;
use FS::pay_batch;
-use FS::cust_pay_batch;
-use FS::cust_bill_event;
use FS::cust_event;
use FS::part_pkg;
use FS::cust_bill_pay;
-use FS::cust_bill_pay_batch;
-use FS::part_bill_event;
use FS::payby;
use FS::bill_batch;
use FS::cust_bill_batch;
use FS::cust_credit_bill_pkg;
use FS::discount_plan;
use FS::cust_bill_void;
+use FS::reason;
+use FS::reason_type;
use FS::L10N;
$DEBUG = 0;
$me = '[FS::cust_bill]';
-#ask FS::UID to run this stuff for us later
-FS::UID->install_callback( sub {
- my $conf = new FS::Conf; #global
- $date_format = $conf->config('date_format') || '%x'; #/YY
-} );
-
=head1 NAME
FS::cust_bill - Object methods for cust_bill records
$tax_amount = $record->tax;
@lines = $cust_bill->print_text;
- @lines = $cust_bill->print_text $time;
+ @lines = $cust_bill->print_text('time' => $time);
=head1 DESCRIPTION
=back
-Customer info at invoice generation time
+Deprecated fields
=over 4
-=item previous_balance
-
-=item billing_balance
-
-=back
-
-Deprecated
+=item billing_balance - the customer's balance immediately before generating
+this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
+to determine the customer's balance at a specific time.
-=over 4
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one. DEPRECATED.
-=item printed - deprecated
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
=back
=item promised_date - customer promised payment date, for collection
+=item pending - invoice is still being generated, empty or 'Y'
+
=back
=head1 METHODS
=cut
sub table { 'cust_bill'; }
-sub notice_name { 'Invoice'; }
+sub template_conf { 'invoice_'; }
-sub cust_linked { $_[0]->cust_main_custnum; }
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+ my $self = shift;
+ $self->conf->config('notice_name') || 'Invoice'
+}
+
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
sub cust_unlinked_msg {
my $self = shift;
"WARNING: can't find cust_main.custnum ". $self->custnum.
}
-=item void
+=item void [ REASON [ , REPROCESS_CDRS ] ]
Voids this invoice: deletes the invoice and adds a record of the voided invoice
to the FS::cust_bill_void table (and related tables starting from
sub void {
my $self = shift;
my $reason = scalar(@_) ? shift : '';
+ my $reprocess_cdrs = scalar(@_) ? shift : '';
+
+ unless (ref($reason) || !$reason) {
+ $reason = FS::reason->new_or_existing(
+ 'class' => 'I',
+ 'type' => 'Invoice void',
+ 'reason' => $reason
+ );
+ }
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my $cust_bill_void = new FS::cust_bill_void ( {
map { $_ => $self->get($_) } $self->fields
} );
- $cust_bill_void->reason($reason);
+ $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
my $error = $cust_bill_void->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
}
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
- my $error = $cust_bill_pkg->void($reason);
+ my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
}
- $error = $self->delete;
+ $error = $self->_delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
-=item delete
-
-This method now works but you probably shouldn't use it. Instead, apply a
-credit against the invoice, or use the new void method.
-
-Using this method to delete invoices outright is really, really bad. There
-would be no record you ever posted this invoice, and there are no check to
-make sure charged = 0 or that there are no associated cust_bill_pkg records.
-
-Really, don't use it.
-
-=cut
+# removed docs entirely and renamed method to _delete to further indicate it is
+# internal-only and discourage use
+#
+# =item delete
+#
+# DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
+# the B<void> method.
+#
+# This is only for internal use by V<void>, which is what you should be using.
+#
+# DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly
+# wrong. Use B<void>, that's what it is for. Really. This means you.
+#
+# =cut
-sub delete {
+sub _delete {
my $self = shift;
return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
my $dbh = dbh;
foreach my $table (qw(
- cust_bill_event
- cust_event
cust_credit_bill
- cust_bill_pay
- cust_pay_batch
cust_bill_pay_batch
+ cust_bill_pay
cust_bill_batch
cust_bill_pkg
)) {
+ #cust_event # problematic
+ #cust_pay_batch # unnecessary
foreach my $linked ( $self->$table() ) {
my $error = $linked->delete;
#return "Can't change _date!" unless $old->_date eq $new->_date;
return "Can't change _date" unless $old->_date == $new->_date;
return "Can't change charged" unless $old->charged == $new->charged
+ || $old->pending eq 'Y'
|| $old->charged == 0
|| $new->{'Hash'}{'cc_surcharge_replace_hack'};
|| $self->ut_enum('closed', [ '', 'Y' ])
|| $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
|| $self->ut_numbern('agent_invid') #varchar?
+ || $self->ut_flag('pending')
;
return $error if $error;
sub display_invnum {
my $self = shift;
- my $conf = $self->conf;
- if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ if ( $self->agent_invid
+ && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
return $self->agent_invid;
} else {
return $self->invnum;
}
}
+=item previous_bill
+
+Returns the customer's last invoice before this one.
+
+=cut
+
+sub previous_bill {
+ my $self = shift;
+ if ( !$self->get('previous_bill') ) {
+ $self->set('previous_bill', qsearchs({
+ 'table' => 'cust_bill',
+ 'hashref' => { 'custnum' => $self->custnum,
+ '_date' => { op=>'<', value=>$self->_date } },
+ 'order_by' => 'ORDER BY _date DESC LIMIT 1',
+ }) );
+ }
+ $self->get('previous_bill');
+}
+
+=item following_bill
+
+Returns the customer's invoice that follows this one
+
+=cut
+
+sub following_bill {
+ my $self = shift;
+ if (!$self->get('following_bill')) {
+ $self->set('following_bill', qsearchs({
+ table => 'cust_bill',
+ hashref => {
+ custnum => $self->custnum,
+ invnum => { op => '>', value => $self->invnum },
+ },
+ order_by => 'ORDER BY invnum ASC LIMIT 1',
+ }));
+ }
+ $self->get('following_bill');
+}
+
=item previous
-Returns a list consisting of the total previous balance for this customer,
+Returns a list consisting of the total previous balance for this customer,
followed by the previous outstanding invoices (as FS::cust_bill objects also).
=cut
sub previous {
my $self = shift;
- my $total = 0;
- my @cust_bill = sort { $a->_date <=> $b->_date }
- grep { $_->owed != 0 }
- qsearch( 'cust_bill', { 'custnum' => $self->custnum,
- '_date' => { op=>'<', value=>$self->_date },
- } )
- ;
- foreach ( @cust_bill ) { $total += $_->owed; }
- $total, @cust_bill;
+ # simple memoize; we use this a lot
+ if (!$self->get('previous')) {
+ my $total = 0;
+ my @cust_bill = sort { $a->_date <=> $b->_date }
+ grep { $_->owed != 0 }
+ qsearch( 'cust_bill', { 'custnum' => $self->custnum,
+ #'_date' => { op=>'<', value=>$self->_date },
+ 'invnum' => { op=>'<', value=>$self->invnum },
+ } )
+ ;
+ foreach ( @cust_bill ) { $total += $_->owed; }
+ $self->set('previous', [$total, @cust_bill]);
+ }
+ return @{ $self->get('previous') };
}
=item enable_previous
sub cust_bill_pkg {
my $self = shift;
qsearch(
- { 'table' => 'cust_bill_pkg',
+ {
+ 'select' => 'cust_bill_pkg.*, pkg_category.categoryname',
+ 'table' => 'cust_bill_pkg',
+ 'addl_from' => ' LEFT JOIN cust_pkg USING ( pkgnum ) '.
+ ' LEFT JOIN part_pkg USING ( pkgpart ) '.
+ ' LEFT JOIN pkg_class USING ( classnum ) '.
+ ' LEFT JOIN pkg_category USING ( categorynum ) ',
'hashref' => { 'invnum' => $self->invnum },
- 'order_by' => 'ORDER BY billpkgnum',
+ 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
+ # the AUTLOADED FK search. or should
+ # that default to ORDER by the pkey?
}
);
}
@open;
}
-=item cust_bill_event
-
-Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub cust_bill_event {
- my $self = shift;
- qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
-}
-
-=item num_cust_bill_event
-
-Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub num_cust_bill_event {
- my $self = shift;
- my $sql =
- "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
- my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
- $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
- $sth->fetchrow_arrayref->[0];
-}
-
=item cust_event
Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
my $sql =
"SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
" WHERE tablenum = ? AND eventtable = 'cust_bill'";
- my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
+ my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
$sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
$sth->fetchrow_arrayref->[0];
}
Returns the customer (see L<FS::cust_main>) for this invoice.
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
+
+Returns a list: an empty list on success or a list of errors.
+
=cut
-sub cust_main {
+sub suspend {
my $self = shift;
- qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+ grep { $_->suspend(@_) }
+ grep {! $_->getfield('cancel') }
+ $self->cust_pkg;
+
}
=item cust_suspend_if_balance_over AMOUNT
}
}
-=item cust_credit
+=item cancel
-Depreciated. See the cust_credited method.
-
- #Returns a list consisting of the total previous credited (see
- #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
- #outstanding credits (FS::cust_credit objects).
+Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
=cut
-sub cust_credit {
- use Carp;
- croak "FS::cust_bill->cust_credit depreciated; see ".
- "FS::cust_bill->cust_credit_bill";
- #my $self = shift;
- #my $total = 0;
- #my @cust_credit = sort { $a->_date <=> $b->_date }
- # grep { $_->credited != 0 && $_->_date < $self->_date }
- # qsearch('cust_credit', { 'custnum' => $self->custnum } )
- #;
- #foreach (@cust_credit) { $total += $_->credited; }
- #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated. See the cust_bill_pay method.
+sub cancel {
+ my( $self, %opt ) = @_;
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
+ warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+ join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+ if $DEBUG;
-=cut
+ return ( 'Access denied' )
+ unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
-sub cust_pay {
- use Carp;
- croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
- #my $self = shift;
- #sort { $a->_date <=> $b->_date }
- # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
- #;
-}
+ my @pkgs = $self->cust_pkg;
-sub cust_pay_batch {
- my $self = shift;
- qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
-}
+ if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
+ $opt{nobill} = 1;
+ my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
+ warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+ if $error;
+ }
-sub cust_bill_pay_batch {
- my $self = shift;
- qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+ grep { $_ }
+ map { $_->cancel(%opt) }
+ grep { ! $_->getfield('cancel') }
+ @pkgs;
}
=item cust_bill_pay
=item discount_plans
-Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
hash keyed by term length.
=cut
$balance;
}
+=item owed_on_invoice
+
+Returns the amount to be displayed as the "Balance Due" on this
+invoice. Amount returned depends on conf flags for invoicing
+
+See L<FS::cust_bill::owed> for the true amount currently owed
+
+=cut
+
+sub owed_on_invoice {
+ my $self = shift;
+
+ #return $self->owed()
+ # unless $self->conf->exists('previous_balance-payments_since')
+
+ # Add charges from this invoice
+ my $owed = $self->charged();
+
+ # Add carried balances from previous invoices
+ # If previous items aren't to be displayed on the invoice,
+ # _items_previous() is aware of this and responds appropriately.
+ $owed += $_->{amount} for $self->_items_previous();
+
+ # Subtract payments and credits displayed on this invoice
+ $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
+
+ return $owed;
+}
+
sub owed_pkgnum {
my( $self, $pkgnum ) = @_;
=item apply_payments_and_credits [ OPTION => VALUE ... ]
Applies unapplied payments and credits to this invoice.
+Payments with the no_auto_apply flag set will not be applied.
A hash of optional arguments may be passed. Currently "manual" is supported.
If true, a payment receipt is sent instead of a statement when
$self->select_for_update; #mutex
- my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
+ my @payments = grep { $_->unapplied > 0 }
+ grep { !$_->no_auto_apply }
+ $self->cust_main->cust_pay;
my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
if ( $conf->exists('pkg-balances') ) {
);
my $max_credit_weight =
max( map { $_->part_pkg->credit_weight || 0 }
- grep { $_ }
+ grep { $_ }
map { $_->cust_pkg }
@open_lineitems
);
} else {
$app = 'credit';
}
-
+
} elsif ( @payments ) {
$app = 'pay';
} elsif ( @credits ) {
}
-=item generate_email OPTION => VALUE ...
-
-Options:
-
-=over 4
-
-=item from
-
-sender address, required
+=item send HASHREF
-=item tempate
-
-alternate template name, optional
-
-=item print_text
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes. See L<FS::cust_main_invoice>.
-text attachment arrayref, optional
+Options can be passed as a hashref. Positional parameters are no longer
+allowed.
-=item subject
+I<template>: a suffix for alternate invoices
-email subject, optional
+I<agentnum>: obsolete, now does nothing.
-=item notice_name
+I<from> overrides the default email invoice From: address.
-notice name instead of "Invoice", optional
+I<amount>: obsolete, does nothing
-=back
+I<notice_name> overrides "Invoice" as the name of the sent document
+(templates from 10/2009 or newer required).
-Returns an argument list to be passed to L<FS::Misc::send_email>.
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
=cut
-use MIME::Entity;
-
-sub generate_email {
-
+sub send {
my $self = shift;
- my %args = @_;
+ my $opt = ref($_[0]) ? $_[0] : +{ @_ };
my $conf = $self->conf;
- my $me = '[FS::cust_bill::generate_email]';
-
- my %return = (
- 'from' => $args{'from'},
- 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
- );
-
- my %opt = (
- 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
- 'template' => $args{'template'},
- 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
- 'no_coupon' => $args{'no_coupon'},
- );
-
my $cust_main = $self->cust_main;
- if (ref($args{'to'}) eq 'ARRAY') {
- $return{'to'} = $args{'to'};
- } else {
- $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
- $cust_main->invoicing_list
- ];
- }
-
- if ( $conf->exists('invoice_html') ) {
-
- warn "$me creating HTML/text multipart message"
- if $DEBUG;
-
- $return{'nobody'} = 1;
+ my @invoicing_list = $cust_main->invoicing_list;
- my $alternative = build MIME::Entity
- 'Type' => 'multipart/alternative',
- #'Encoding' => '7bit',
- 'Disposition' => 'inline'
- ;
+ $self->email($opt)
+ if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+ && ! $cust_main->invoice_noemail;
- my $data;
- if ( $conf->exists('invoice_email_pdf')
- and scalar($conf->config('invoice_email_pdf_note')) ) {
+ $self->print($opt)
+ if grep { $_ eq 'POST' } @invoicing_list; #postal
- warn "$me using 'invoice_email_pdf_note' in multipart message"
- if $DEBUG;
- $data = [ map { $_ . "\n" }
- $conf->config('invoice_email_pdf_note')
- ];
+ #this has never been used post-$ORIGINAL_ISP afaik
+ $self->fax_invoice($opt)
+ if grep { $_ eq 'FAX' } @invoicing_list; #fax
- } else {
+ '';
- warn "$me not using 'invoice_email_pdf_note' in multipart message"
- if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $data = $args{'print_text'};
- } else {
- $data = [ $self->print_text(\%opt) ];
- }
+}
- }
+sub email {
+ my $self = shift;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die ref($self). '->email called with positional parameters';
+ }
- $alternative->attach(
- 'Type' => 'text/plain',
- 'Encoding' => 'quoted-printable',
- #'Encoding' => '7bit',
- 'Data' => $data,
- 'Disposition' => 'inline',
- );
+ my $conf = $self->conf;
+ my $from = delete $opt->{from};
- my $htmldata;
- my $image = '';
- my $barcode = '';
- if ( $conf->exists('invoice_email_pdf')
- and scalar($conf->config('invoice_email_pdf_note')) ) {
+ # this is where we set the From: address
+ $from ||= $self->_agent_invoice_from || #XXX should go away
+ $conf->invoice_from_full( $self->cust_main->agentnum );
- $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
+ my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
+ if ( ! @invoicing_list ) { #no recipients
+ if ( $conf->exists('cust_bill-no_recipients-error') ) {
+ die 'No recipients for customer #'. $self->custnum;
} else {
-
- $args{'from'} =~ /\@([\w\.\-]+)/;
- my $from = $1 || 'example.com';
- my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-
- my $logo;
- my $agentnum = $cust_main->agentnum;
- if ( defined($args{'template'}) && length($args{'template'})
- && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
- )
- {
- $logo = 'logo_'. $args{'template'}. '.png';
- } else {
- $logo = "logo.png";
- }
- my $image_data = $conf->config_binary( $logo, $agentnum);
-
- $image = build MIME::Entity
- 'Type' => 'image/png',
- 'Encoding' => 'base64',
- 'Data' => $image_data,
- 'Filename' => 'logo.png',
- 'Content-ID' => "<$content_id>",
- ;
-
- if ($conf->exists('invoice-barcode')) {
- my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
- $barcode = build MIME::Entity
- 'Type' => 'image/png',
- 'Encoding' => 'base64',
- 'Data' => $self->invoice_barcode(0),
- 'Filename' => 'barcode.png',
- 'Content-ID' => "<$barcode_content_id>",
- ;
- $opt{'barcode_cid'} = $barcode_content_id;
- }
-
- $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
+ #default: better to notify this person than silence
+ @invoicing_list = ($from);
}
+ }
- $alternative->attach(
- 'Type' => 'text/html',
- 'Encoding' => 'quoted-printable',
- 'Data' => [ '<html>',
- ' <head>',
- ' <title>',
- ' '. encode_entities($return{'subject'}),
- ' </title>',
- ' </head>',
- ' <body bgcolor="#e8e8e8">',
- $htmldata,
- ' </body>',
- '</html>',
- ],
- 'Disposition' => 'inline',
- #'Filename' => 'invoice.pdf',
- );
+ $self->SUPER::email( {
+ 'from' => $from,
+ 'to' => \@invoicing_list,
+ %$opt,
+ });
+}
- my @otherparts = ();
- if ( $cust_main->email_csv_cdr ) {
+#this stays here for now because its explicitly used as
+# FS::cust_bill::queueable_email
+sub queueable_email {
+ my %opt = @_;
- push @otherparts, build MIME::Entity
- 'Type' => 'text/csv',
- 'Encoding' => '7bit',
- 'Data' => [ map { "$_\n" }
- $self->call_details('prepend_billed_number' => 1)
- ],
- 'Disposition' => 'attachment',
- 'Filename' => 'usage-'. $self->invnum. '.csv',
- ;
+ my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
+ or die "invalid invoice number: " . $opt{invnum};
- }
+ $self->set('mode', $opt{mode})
+ if $opt{mode};
- if ( $conf->exists('invoice_email_pdf') ) {
-
- #attaching pdf too:
- # multipart/mixed
- # multipart/related
- # multipart/alternative
- # text/plain
- # text/html
- # image/png
- # application/pdf
-
- my $related = build MIME::Entity 'Type' => 'multipart/related',
- 'Encoding' => '7bit';
-
- #false laziness w/Misc::send_email
- $related->head->replace('Content-type',
- $related->mime_type.
- '; boundary="'. $related->head->multipart_boundary. '"'.
- '; type=multipart/alternative'
- );
+ my %args = map {$_ => $opt{$_}}
+ grep { $opt{$_} }
+ qw( from notice_name no_coupon template );
- $related->add_part($alternative);
+ my $error = $self->email( \%args );
+ die $error if $error;
- $related->add_part($image) if $image;
+}
- my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
+sub email_subject {
+ my $self = shift;
+ my $conf = $self->conf;
- $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
+ #my $template = scalar(@_) ? shift : '';
+ #per-template?
- } else {
+ my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+ || 'Invoice';
- #no other attachment:
- # multipart/related
- # multipart/alternative
- # text/plain
- # text/html
- # image/png
+ my $cust_main = $self->cust_main;
+ my $name = $cust_main->name;
+ my $name_short = $cust_main->name_short;
+ my $invoice_number = $self->invnum;
+ my $invoice_date = $self->_date_pretty;
- $return{'content-type'} = 'multipart/related';
- if ($conf->exists('invoice-barcode') && $barcode) {
- $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
- } else {
- $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
- }
- $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
- #$return{'disposition'} = 'inline';
+ eval qq("$subject");
+}
- }
-
- } else {
+sub pdf_filename {
+ my $self = shift;
+ 'Invoice-'. $self->invnum. '.pdf';
+}
- if ( $conf->exists('invoice_email_pdf') ) {
- warn "$me creating PDF attachment"
- if $DEBUG;
+=item lpr_data HASHREF
- #mime parts arguments a la MIME::Entity->build().
- $return{'mimeparts'} = [
- { $self->mimebuild_pdf(\%opt) }
- ];
- }
-
- if ( $conf->exists('invoice_email_pdf')
- and scalar($conf->config('invoice_email_pdf_note')) ) {
+Returns the postscript or plaintext for this invoice as an arrayref.
- warn "$me using 'invoice_email_pdf_note'"
- if $DEBUG;
- $return{'body'} = [ map { $_ . "\n" }
- $conf->config('invoice_email_pdf_note')
- ];
+Options must be passed as a hashref. Positional parameters are no longer
+allowed.
- } else {
+I<template>, if specified, is the name of a suffix for alternate invoices.
- warn "$me not using 'invoice_email_pdf_note'"
- if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $return{'body'} = $args{'print_text'};
- } else {
- $return{'body'} = [ $self->print_text(\%opt) ];
- }
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
- }
+=cut
+sub lpr_data {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ # nobody does this anyway
+ die "FS::cust_bill::lpr_data called with positional parameters";
}
- %return;
-
+ my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
+ [ $self->$method( $opt ) ];
}
-=item mimebuild_pdf
+=item print HASHREF
+
+Prints this invoice.
+
+Options must be passed as a hashref.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
-Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
=cut
-sub mimebuild_pdf {
+sub print {
my $self = shift;
- (
- 'Type' => 'application/pdf',
- 'Encoding' => 'base64',
- 'Data' => [ $self->print_pdf(@_) ],
- 'Disposition' => 'attachment',
- 'Filename' => 'invoice-'. $self->invnum. '.pdf',
- );
+ return if $self->hide;
+ my $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::print called with positional parameters";
+ }
+
+ my $lpr = delete $opt->{lpr};
+ if($conf->exists('invoice_print_pdf')) {
+ # Add the invoice to the current batch.
+ $self->batch_invoice($opt);
+ }
+ else {
+ do_print(
+ $self->lpr_data($opt),
+ 'agentnum' => $self->cust_main->agentnum,
+ 'lpr' => $lpr,
+ );
+ }
}
-=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
+=item fax_invoice HASHREF
-Sends this invoice to the destinations configured for this customer: sends
-email, prints and/or faxes. See L<FS::cust_main_invoice>.
+Faxes this invoice.
-Options can be passed as a hashref (recommended) or as a list of up to
-four values for templatename, agentnum, invoice_from and amount.
+Options must be passed as a hashref.
I<template>, if specified, is the name of a suffix for alternate invoices.
-I<agentnum>, if specified, means that this invoice will only be sent for customers
-of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
-single agent) or an arrayref of agentnums.
-
-I<invoice_from>, if specified, overrides the default email invoice From: address.
-
-I<amount>, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
-
I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
=cut
-sub queueable_send {
- my %opt = @_;
+sub fax_invoice {
+ my $self = shift;
+ return if $self->hide;
+ my $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::fax_invoice called with positional parameters";
+ }
- my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
- or die "invalid invoice number: " . $opt{invnum};
+ die 'FAX invoice destination not (yet?) supported with plain text invoices.'
+ unless $conf->exists('invoice_latex');
- my @args = ( $opt{template}, $opt{agentnum} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
+ my $dialstring = $self->cust_main->getfield('fax');
+ #Check $dialstring?
- my $error = $self->send( @args );
+ my $error = send_fax( 'docdata' => $self->lpr_data($opt),
+ 'dialstring' => $dialstring,
+ );
die $error if $error;
}
-sub send {
- my $self = shift;
- my $conf = $self->conf;
-
- my( $template, $invoice_from, $notice_name );
- my $agentnums = '';
- my $balance_over = 0;
+=item batch_invoice [ HASHREF ]
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- if ( $agentnums = $opt->{'agentnum'} ) {
- $agentnums = [ $agentnums ] unless ref($agentnums);
- }
- $invoice_from = $opt->{'invoice_from'};
- $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
- $notice_name = $opt->{'notice_name'};
- } else {
- $template = scalar(@_) ? shift : '';
- if ( scalar(@_) && $_[0] ) {
- $agentnums = ref($_[0]) ? shift : [ shift ];
- }
- $invoice_from = shift if scalar(@_);
- $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
- }
+Place this invoice into the open batch (see C<FS::bill_batch>). If there
+isn't an open batch, one will be created.
- my $cust_main = $self->cust_main;
+HASHREF may contain any options to be passed to C<print_pdf>.
- return 'N/A' unless ! $agentnums
- or grep { $_ == $cust_main->agentnum } @$agentnums;
+=cut
- return ''
- unless $cust_main->total_owed_date($self->_date) > $balance_over;
+sub batch_invoice {
+ my ($self, $opt) = @_;
+ my $bill_batch = $self->get_open_bill_batch;
+ my $cust_bill_batch = FS::cust_bill_batch->new({
+ batchnum => $bill_batch->batchnum,
+ invnum => $self->invnum,
+ });
+ if ( $self->mode ) {
+ $opt->{mode} ||= $self->mode;
+ $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
+ }
+ return $cust_bill_batch->insert($opt);
+}
- $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $cust_main->agentnum );
-
- my %opt = (
- 'template' => $template,
- 'invoice_from' => $invoice_from,
- 'notice_name' => ( $notice_name || 'Invoice' ),
- );
-
- my @invoicing_list = $cust_main->invoicing_list;
-
- #$self->email_invoice(\%opt)
- $self->email(\%opt)
- if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
- && ! $self->invoice_noemail;
-
- #$self->print_invoice(\%opt)
- $self->print(\%opt)
- if grep { $_ eq 'POST' } @invoicing_list; #postal
-
- $self->fax_invoice(\%opt)
- if grep { $_ eq 'FAX' } @invoicing_list; #fax
-
- '';
-
-}
-
-=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
-
-Emails this invoice.
-
-Options can be passed as a hashref (recommended) or as a list of up to
-two values for templatename and invoice_from.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<invoice_from>, if specified, overrides the default email invoice From: address.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub queueable_email {
- my %opt = @_;
-
- my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
- or die "invalid invoice number: " . $opt{invnum};
-
- my %args = ( 'template' => $opt{template} );
- $args{$_} = $opt{$_}
- foreach grep { exists($opt{$_}) && $opt{$_} }
- qw( invoice_from notice_name no_coupon );
-
- my $error = $self->email( \%args );
- die $error if $error;
-
-}
-
-#sub email_invoice {
-sub email {
- my $self = shift;
- return if $self->hide;
- my $conf = $self->conf;
-
- my( $template, $invoice_from, $notice_name, $no_coupon );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $invoice_from = $opt->{'invoice_from'};
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- $no_coupon = $opt->{'no_coupon'} || 0;
- } else {
- $template = scalar(@_) ? shift : '';
- $invoice_from = shift if scalar(@_);
- $notice_name = 'Invoice';
- $no_coupon = 0;
- }
-
- $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum );
-
- my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
- $self->cust_main->invoicing_list;
-
- if ( ! @invoicing_list ) { #no recipients
- if ( $conf->exists('cust_bill-no_recipients-error') ) {
- die 'No recipients for customer #'. $self->custnum;
- } else {
- #default: better to notify this person than silence
- @invoicing_list = ($invoice_from);
- }
- }
-
- my $subject = $self->email_subject($template);
-
- my $error = send_email(
- $self->generate_email(
- 'from' => $invoice_from,
- 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
- 'subject' => $subject,
- 'template' => $template,
- 'notice_name' => $notice_name,
- 'no_coupon' => $no_coupon,
- )
- );
- die "can't email invoice: $error\n" if $error;
- #die "$error\n" if $error;
-
-}
-
-sub email_subject {
- my $self = shift;
- my $conf = $self->conf;
-
- #my $template = scalar(@_) ? shift : '';
- #per-template?
-
- my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
- || 'Invoice';
-
- my $cust_main = $self->cust_main;
- my $name = $cust_main->name;
- my $name_short = $cust_main->name_short;
- my $invoice_number = $self->invnum;
- my $invoice_date = $self->_date_pretty;
-
- eval qq("$subject");
-}
-
-=item lpr_data HASHREF | [ TEMPLATE ]
-
-Returns the postscript or plaintext for this invoice as an arrayref.
-
-Options can be passed as a hashref (recommended) or as a single optional value
-for template.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub lpr_data {
- my $self = shift;
- my $conf = $self->conf;
- my( $template, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
- }
-
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
- [ $self->$method( \%opt ) ];
-}
-
-=item print HASHREF | [ TEMPLATE ]
-
-Prints this invoice.
-
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-#sub print_invoice {
-sub print {
- my $self = shift;
- return if $self->hide;
- my $conf = $self->conf;
-
- my( $template, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
- }
-
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- if($conf->exists('invoice_print_pdf')) {
- # Add the invoice to the current batch.
- $self->batch_invoice(\%opt);
- }
- else {
- do_print $self->lpr_data(\%opt);
- }
-}
-
-=item fax_invoice HASHREF | [ TEMPLATE ]
-
-Faxes this invoice.
-
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub fax_invoice {
- my $self = shift;
- return if $self->hide;
- my $conf = $self->conf;
-
- my( $template, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $notice_name = 'Invoice';
- }
-
- die 'FAX invoice destination not (yet?) supported with plain text invoices.'
- unless $conf->exists('invoice_latex');
-
- my $dialstring = $self->cust_main->getfield('fax');
- #Check $dialstring?
-
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
- 'dialstring' => $dialstring,
- );
- die $error if $error;
-
-}
-
-=item batch_invoice [ HASHREF ]
-
-Place this invoice into the open batch (see C<FS::bill_batch>). If there
-isn't an open batch, one will be created.
-
-=cut
-
-sub batch_invoice {
- my ($self, $opt) = @_;
- my $bill_batch = $self->get_open_bill_batch;
- my $cust_bill_batch = FS::cust_bill_batch->new({
- batchnum => $bill_batch->batchnum,
- invnum => $self->invnum,
- });
- return $cust_bill_batch->insert($opt);
-}
-
-=item get_open_batch
+=item get_open_batch
Returns the currently open batch as an FS::bill_batch object, creating a new
one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
return $batch;
}
-=item ftp_invoice [ TEMPLATENAME ]
+=item ftp_invoice [ TEMPLATENAME ]
Sends this invoice data via FTP.
);
}
-=item spool_invoice [ TEMPLATENAME ]
+=item spool_invoice [ TEMPLATENAME ]
Spools this invoice data (see L<FS::spool_csv>)
);
}
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
- my $self = shift;
-
- return ''
- if scalar(
- grep { $_->owed > 0 }
- qsearch('cust_bill', {
- 'custnum' => $self->custnum,
- #'_date' => { op=>'>', value=>$self->_date },
- 'invnum' => { op=>'>', value=>$self->invnum },
- } )
- );
-
- $self->send(@_);
-}
-
=item send_csv OPTION => VALUE, ...
Sends invoice as a CSV data-file to a remote host with the specified protocol.
my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
mkdir $spooldir, 0700 unless -d $spooldir;
+ # don't localize dates here, they're a defined format
my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
my $file = "$spooldir/$tracctnum.csv";
-
+
my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
open(CSV, ">$file") or die "can't open $file: $!";
=item agent_spools - if set to a true value, will spool to per-agent files
rather than a single global file
-=item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will
+=item upload_targetnum - if set to a target (see L<FS::upload_target>), will
append to that spool. L<FS::Cron::upload> will then send the spool file to
that destination.
=item balanceover - if set, only spools the invoice if the total amount owed on
this invoice and all older invoices is greater than the specified amount.
+=item time - the "current time". Controls the printing of past due messages
+in the ICS format.
+
=back
=cut
sub spool_csv {
my($self, %opt) = @_;
+ my $time = $opt{'time'} || time;
my $cust_main = $self->cust_main;
if ( $opt{'dest'} ) {
my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
mkdir $spooldir, 0700 unless -d $spooldir;
- my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
+ my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
my $file;
if ( $opt{'agent_spools'} ) {
$file = 'spool';
}
- if ( $opt{'ftp_targetnum'} ) {
- $spooldir .= '/target'.$opt{'ftp_targetnum'};
+ if ( $opt{'upload_targetnum'} ) {
+ $spooldir .= '/target'.$opt{'upload_targetnum'};
mkdir $spooldir, 0700 unless -d $spooldir;
} # otherwise it just goes into export.xxx/cust_bill
}
$file = "$spooldir/$file.csv";
-
- my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
+
+ my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
open(CSV, ">>$file") or die "can't open $file: $!";
flock(CSV, LOCK_EX);
seek(CSV, 0, 2);
}
- print CSV $detail;
+ print CSV $detail if defined($detail);
flock(CSV, LOCK_UN);
close CSV;
If I<format> is not specified or "default", the fields of the CSV file are as
follows:
-record_type, invnum, custnum, _date, charged, first, last, company, address1,
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
address2, city, state, zip, country, pkg, setup, recur, sdate, edate
=over 4
9 | Grouping Code | GROUP | CHAR | 2
10 | User Defined | ACCT CODE | CHAR | 15
-If format is 'oneline', there is no detail file. Each invoice has a
+If format is 'oneline', there is no detail file. Each invoice has a
header line only, with the fields:
Agent number, agent name, customer number, first name, last name, address
line 1, address line 2, city, state, zip, invoice date, invoice number,
-amount charged, amount due,
+amount charged, amount due, previous balance, due date.
and then, for each line item, three columns containing the package number,
description, and amount.
-If format is 'bridgestone', there is no detail file. Each invoice has a
+If format is 'bridgestone', there is no detail file. Each invoice has a
header line with the following fields in a fixed-width format:
Customer number (in display format), date, name (first last), company,
address 1, address 2, city, state, zip.
This is a mailing list format, and has no per-invoice fields. To avoid
-sending redundant notices, the spooling event should have a "once" or
+sending redundant notices, the spooling event should have a "once" or
"once_percust_every" condition.
=cut
sub print_csv {
my($self, %opt) = @_;
-
+
eval "use Text::CSV_XS";
die $@ if $@;
my $cust_main = $self->cust_main;
my $csv = Text::CSV_XS->new({'always_quote'=>1});
+ my $format = lc($opt{'format'});
- if ( lc($opt{'format'}) eq 'billco' ) {
+ my $time = $opt{'time'} || time;
+
+ $self->set('_template', $opt{template})
+ if exists $opt{template};
+
+ my $tracctnum = ''; #leaking out from billco-specific sections :/
+ if ( $format eq 'billco' ) {
+
+ my $account_num =
+ $self->conf->config('billco-account_num', $cust_main->agentnum);
+
+ $tracctnum = $account_num eq 'display_custnum'
+ ? $cust_main->display_custnum
+ : $opt{'tracctnum'};
my $taxtotal = 0;
$taxtotal += $_->{'amount'} foreach $self->_items_tax;
- my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+ my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
my( $previous_balance, @unused ) = $self->previous; #previous balance
my $pmt_cr_applied = 0;
$pmt_cr_applied += $_->{'amount'}
- foreach ( $self->_items_payments, $self->_items_credits ) ;
+ foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
$csv->combine(
'', # 1 | N/A-Leave Empty CHAR 2
'', # 2 | N/A-Leave Empty CHAR 15
- $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
+ $tracctnum, # 3 | Transaction Account No CHAR 15
$self->invnum, # 4 | Transaction Invoice No CHAR 15
$cust_main->zip, # 5 | Transaction Zip Code CHAR 5
$cust_main->company, # 6 | Transaction Company Bill To CHAR 30
'0', # 29 | Other Taxes & Fees*** NUM* 9
);
- } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
-
- my ($previous_balance) = $self->previous;
+ } elsif ( $format eq 'oneline' ) { #name
+
+ my ($previous_balance) = $self->previous;
+ $previous_balance = sprintf('%.2f', $previous_balance);
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
my @items = map {
- ($_->{pkgnum} || ''),
- $_->{description},
- $_->{amount}
- } $self->_items_pkg;
+ $_->{pkgnum},
+ $_->{description},
+ $_->{amount}
+ }
+ $self->_items_pkg, #_items_nontax? no sections or anything
+ # with this format
+ $self->_items_tax;
$csv->combine(
$cust_main->agentnum,
$self->custnum,
$cust_main->first,
$cust_main->last,
+ $cust_main->company,
$cust_main->address1,
$cust_main->address2,
$cust_main->city,
$self->invnum,
$self->charged,
$totaldue,
+ $previous_balance,
+ $self->due_date2str("%x"),
@items,
);
- } elsif ( lc($opt{'format'}) eq 'bridgestone' ) {
+ } elsif ( $format eq 'bridgestone' ) {
# bypass the CSV stuff and just return this
- my $longdate = time2str('%B %d, %Y', time); #current time, right?
+ my $longdate = time2str('%B %d, %Y', $time); #current time, right?
my $zip = $cust_main->zip;
$zip =~ s/\D//;
my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
'' #detail
);
- } else {
-
+ } elsif ( $format eq 'ics' ) {
+
+ my $bill = $cust_main->bill_location;
+ my $zip = $bill->zip;
+ my $zip4 = '';
+
+ $zip =~ s/\D//;
+ if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
+ $zip = $1;
+ $zip4 = $2;
+ }
+
+ # minor false laziness with print_generic
+ my ($previous_balance) = $self->previous;
+ my $balance_due = $self->owed + $previous_balance;
+ my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
+ my $credit_total = sum(0, map { $_->{'amount'} } $self->_items_credits);
+
+ my $past_due = '';
+ if ( $self->due_date and $time >= $self->due_date ) {
+ $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
+ }
+
+ # again, bypass CSV
+ my $header = sprintf(
+ '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
+ $cust_main->display_custnum, #BID
+ uc($cust_main->first), #FNAME
+ uc($cust_main->last), #LNAME
+ '00', #BATCH, should this ever be anything else?
+ uc($cust_main->company), #COMP
+ uc($bill->address1), #STREET1
+ uc($bill->address2), #STREET2
+ uc($bill->city), #CITY
+ uc($bill->state), #STATE
+ $zip,
+ $zip4,
+ time2str('%Y%m%d', $self->_date), #BILL_DATE
+ $self->due_date2str('%Y%m%d'), #DUE_DATE,
+ ( map {sprintf('%0.2f', $_)}
+ $balance_due, #AMNT_DUE
+ $previous_balance, #PREV_BAL
+ $payment_total, #PYMT_RCVD
+ $credit_total, #CREDITS
+ $previous_balance, #BEG_BAL--is this correct?
+ $self->charged, #NEW_CHRG
+ ),
+ 'img01', #MRKT_MSG?
+ $past_due, #PAST_MSG
+ );
+
+ my @details;
+ my %svc_class = ('' => ''); # maybe cache this more persistently?
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+
+ my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
+ my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
+
+ if ( $cust_pkg ) {
+
+ my @dates = ( $self->_date, undef );
+ if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
+ $dates[1] = $prev->sdate; #questionable
+ }
+
+ # generate an 01 detail for each service
+ my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
+ foreach my $cust_svc ( @svcs ) {
+ $show_pkgnum = ''; # hide it if we're showing svcnums
+
+ my $svcpart = $cust_svc->svcpart;
+ if (!exists($svc_class{$svcpart})) {
+ my $classnum = $cust_svc->part_svc->classnum;
+ my $part_svc_class = FS::part_svc_class->by_key($classnum)
+ if $classnum;
+ $svc_class{$svcpart} = $part_svc_class ?
+ $part_svc_class->classname :
+ '';
+ }
+
+ my @h_label = $cust_svc->label(@dates, 'I');
+ push @details, sprintf('01%-9s%-20s%-47s',
+ $cust_svc->svcnum,
+ $svc_class{$svcpart},
+ $h_label[1],
+ );
+ } #foreach $cust_svc
+ } #if $cust_pkg
+
+ my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
+ if ($cust_bill_pkg->recur > 0) {
+ $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
+ time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
+ }
+ push @details, sprintf('02%-6s%-60s%-10s',
+ $show_pkgnum,
+ $desc,
+ sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
+ );
+ } #foreach $cust_bill_pkg
+
+ # Tag this row so that we know whether this is one page (1), two pages
+ # (2), # or "big" (B). The tag will be stripped off before uploading.
+ if ( scalar(@details) < 12 ) {
+ push @details, '1';
+ } elsif ( scalar(@details) < 58 ) {
+ push @details, '2';
+ } else {
+ push @details, 'B';
+ }
+
+ return join('', $header, @details, "\n");
+
+ } else { # default
+
$csv->combine(
'cust_bill',
$self->invnum,
if ( lc($opt{'format'}) eq 'billco' ) {
my $lineseq = 0;
- foreach my $item ( $self->_items_pkg ) {
+ my %items_opt = ( format => 'template',
+ escape_function => sub { shift } );
+ # I don't know what characters billco actually tolerates in spool entries.
+ # Text::CSV will take care of delimiters, though.
+
+ my @items = ( $self->_items_pkg(%items_opt),
+ $self->_items_fee(%items_opt) );
+ foreach my $item (@items) {
+
+ my $description = $item->{'description'};
+ if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
+ $description .= ': ' . $item->{ext_description}[0];
+ }
$csv->combine(
'', # 1 | N/A-Leave Empty CHAR 2
'', # 2 | N/A-Leave Empty CHAR 15
- $opt{'tracctnum'}, # 3 | Account Number CHAR 15
+ $tracctnum, # 3 | Account Number CHAR 15
$self->invnum, # 4 | Invoice Number CHAR 15
$lineseq++, # 5 | Line Sequence (sort order) NUM 6
- $item->{'description'}, # 6 | Transaction Detail CHAR 100
+ $description, # 6 | Transaction Detail CHAR 100
$item->{'amount'}, # 7 | Amount NUM* 9
'', # 8 | Line Format Control** CHAR 2
'', # 9 | Grouping Code CHAR 2
my($pkg, $setup, $recur, $sdate, $edate);
if ( $cust_bill_pkg->pkgnum ) {
-
+
($pkg, $setup, $recur, $sdate, $edate) = (
$cust_bill_pkg->part_pkg->pkg,
( $cust_bill_pkg->setup != 0
( $cust_bill_pkg->recur != 0
? sprintf("%.2f", $cust_bill_pkg->recur )
: '' ),
- ( $cust_bill_pkg->sdate
+ ( $cust_bill_pkg->sdate
? time2str("%x", $cust_bill_pkg->sdate)
: '' ),
- ($cust_bill_pkg->edate
- ?time2str("%x", $cust_bill_pkg->edate)
+ ($cust_bill_pkg->edate
+ ? time2str("%x", $cust_bill_pkg->edate)
: '' ),
);
-
+
} else { #pkgnum tax
next unless $cust_bill_pkg->setup != 0;
$pkg = $cust_bill_pkg->desc;
$setup = sprintf('%10.2f', $cust_bill_pkg->setup );
( $sdate, $edate ) = ( '', '' );
}
-
+
$csv->combine(
'cust_bill_pkg',
$self->invnum,
}
-=item comp
-
-Pays this invoice with a compliemntary payment. If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
sub comp {
- my $self = shift;
- my $cust_pay = new FS::cust_pay ( {
- 'invnum' => $self->invnum,
- 'paid' => $self->owed,
- '_date' => '',
- 'payby' => 'COMP',
- 'payinfo' => $self->cust_main->payinfo,
- 'paybatch' => '',
- } );
- $cust_pay->insert;
+ croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
}
=item realtime_card
my $cust_main = $self->cust_main;
$options{invnum} = $self->invnum;
-
+
$cust_main->batch_card(%options);
}
sub invoice_barcode {
my ($self, $dir) = (shift,shift);
-
+
my $gdbar = new GD::Barcode('Code39',$self->invnum);
die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
my $gd = $gdbar->plot(Height => 30);
=item invnum_date_pretty
Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
+"Invoice #54 (3/20/2008)".
+
+Intended for back-end context, with regard to translation and date formatting.
=cut
+#note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
+# for backend use (and also does the wrong thing, localizing for end customer
+# instead of backoffice configured date format)
sub invnum_date_pretty {
my $self = shift;
- $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
+ #$self->mt('Invoice #').
+ 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+ $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
}
#sub _items_extra_usage_sections {
my %classnums = ();
my %lines = ();
- my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
my $amount = $detail->amount;
next unless $amount && $amount > 0;
-
+
$sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
$sections{$section}{amount} += $amount; #subtotal
$sections{$section}{calls}++;
$sections{$section}{duration} += $detail->duration;
- my $desc = $detail->regionname;
+ my $desc = $detail->regionname;
my $description = $desc;
$description = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
qw( description_generator header_generator total_generator total_line_generator )
)
: ()
- ),
+ ),
};
}
my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
my $phone_deleted;
$phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
-
+
# DID either activated or ported in; cannot be both for same DID simultaneously
if ($inserted >= $start && $inserted <= $end && $phone_inserted
- && (!$phone_inserted->lnp_status
+ && (!$phone_inserted->lnp_status
|| $phone_inserted->lnp_status eq ''
|| $phone_inserted->lnp_status eq 'native')) {
$num_activated++;
}
else { # this one not so clean, should probably move to (h_)svc_phone
+ local($FS::Record::qsearch_qualify_columns) = 0;
my $phone_portedin = qsearchs( 'h_svc_phone',
- { 'svcnum' => $h_cust_svc->svcnum,
- 'lnp_status' => 'portedin' },
- FS::h_svc_phone->sql_h_searchs($end),
+ { 'svcnum' => $h_cust_svc->svcnum,
+ 'lnp_status' => 'portedin' },
+ FS::h_svc_phone->sql_h_searchs($end),
);
$num_portedin++ if $phone_portedin;
}
# DID either deactivated or ported out; cannot be both for same DID simultaneously
if($deleted >= $start && $deleted <= $end && $phone_deleted
- && (!$phone_deleted->lnp_status
+ && (!$phone_deleted->lnp_status
|| $phone_deleted->lnp_status ne 'portingout')) {
$num_deactivated++;
- }
- elsif($deleted >= $start && $deleted <= $end && $phone_deleted
- && $phone_deleted->lnp_status
+ }
+ elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+ && $phone_deleted->lnp_status
&& $phone_deleted->lnp_status eq 'portingout') {
$num_portedout++;
}
foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
$section->{'header'} = $detail->formatted('format' => $format)
- if($detail->detail eq $section->{'header'});
-
+ if($detail->detail eq $section->{'header'});
+
my $accountcode = $detail->accountcode;
next unless $accountcode;
my %classnums = ();
my %lines = ();
- my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
$usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
$sections{$phonenum}{calls}++;
$sections{$phonenum}{duration} += $detail->duration;
- my $desc = $detail->regionname;
+ my $desc = $detail->regionname;
my $description = $desc;
$description = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
total_line_generator
)
)
- ),
+ ),
};
}
push @lines, $l;
}
}
-
- if($conf->exists('phone_usage_class_summary')) {
+
+ if($conf->exists('phone_usage_class_summary')) {
# this only works with Latex
my @newlines;
my @newsections;
};
$calls_detail{'description'} = 'Calls Detail: '
. $section->{'phonenum'};
- push @newsections, \%calls_detail;
+ push @newsections, \%calls_detail;
}
}
foreach my $newsection ( @newsections ) {
if($newsection->{'post_total'}) { # this means Calls Summary
foreach my $section ( @sections ) {
- next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+ next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
&& !$section->{'post_total'});
my $newdesc = $section->{'description'};
my $tn = $section->{'phonenum'};
}
-sub _items_previous {
+=item _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this
+invoice. Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
my $self = shift;
- my $conf = $self->conf;
- my $cust_main = $self->cust_main;
- my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
- my @b = ();
- foreach ( @pr_cust_bill ) {
- my $date = $conf->exists('invoice_show_prior_due_date')
- ? 'due '. $_->due_date2str($date_format)
- : time2str($date_format, $_->_date);
- push @b, {
- 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
- #'pkgpart' => 'N/A',
- 'pkgnum' => 'N/A',
- 'amount' => sprintf("%.2f", $_->owed),
+ my %opt = @_;
+
+ my $escape = $opt{escape} || sub { $_[0] };
+ my $money_char = $opt{money_char};
+ my $invnum = $self->invnum;
+ my @classes = qsearch({
+ 'table' => 'usage_class',
+ 'select' => 'classnum, classname, SUM(amount) AS amount,'.
+ ' COUNT(*) AS calls, SUM(duration) AS duration',
+ 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+ ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+ 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+ ' GROUP BY classnum, classname, weight'.
+ ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+ ' ORDER BY weight ASC',
+ });
+ my @l;
+ my $section = {
+ description => &{$escape}($self->mt('Usage Summary')),
+ usage_section => 1,
+ subtotal => 0,
+ };
+ foreach my $class (@classes) {
+ $section->{subtotal} += $class->get('amount');
+ push @l, {
+ 'description' => &{$escape}($class->classname),
+ 'amount' => $money_char.sprintf('%.2f', $class->get('amount')),
+ 'quantity' => $class->get('calls'),
+ 'duration' => $class->get('duration'),
+ 'usage_classnum' => $class->classnum,
+ 'section' => $section,
};
}
- @b;
+ $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
+ return @l;
+}
- #{
- # 'description' => 'Previous Balance',
- # #'pkgpart' => 'N/A',
- # 'pkgnum' => 'N/A',
- # 'amount' => sprintf("%10.2f", $pr_total ),
- # 'ext_description' => [ map {
- # "Invoice ". $_->invnum.
- # " (". time2str("%x",$_->_date). ") ".
- # sprintf("%10.2f", $_->owed)
- # } @pr_cust_bill ],
+=item _items_previous()
- #};
-}
+ Returns an array of hashrefs, each hashref representing a line-item on
+ the current bill for previous unpaid invoices.
-sub _items_credits {
- my( $self, %opt ) = @_;
- my $trim_len = $opt{'trim_len'} || 60;
+ keys for each previous_item:
+ - amount (see notes)
+ - pkgnum
+ - description
+ - invnum
+ - _date
- my @b;
- #credits
- foreach ( $self->cust_credited ) {
+ Payments and credits shown on this invoice may vary based on configuraiton.
- #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+ when conf flag previous_balance-payments_since is set:
+ This method works backwards to rebuild the invoice as a snapshot in time.
+ The invoice displayed will have the balances owed, and payments made,
+ reflecting the state of the account at the time of invoice generation.
- my $reason = substr($_->cust_credit->reason, 0, $trim_len);
- $reason .= '...' if length($reason) < length($_->cust_credit->reason);
- $reason = " ($reason) " if $reason;
+=cut
- push @b, {
- #'description' => 'Credit ref\#'. $_->crednum.
- # " (". time2str("%x",$_->cust_credit->_date) .")".
- # $reason,
- 'description' => $self->mt('Credit applied').' '.
- time2str($date_format,$_->cust_credit->_date). $reason,
- 'amount' => sprintf("%.2f",$_->amount),
- };
+sub _items_previous {
+
+ my $self = shift;
+
+ # simple memoize
+ if ($self->get('_items_previous')) {
+ return sort { $a->{_date} <=> $b->{_date} }
+ values %{ $self->get('_items_previous') };
+ }
+
+ # Gets the customer's current balance and outstanding invoices.
+ my ($prev_balance, @open_invoices) = $self->previous;
+
+ my %invoices = map {
+ $_->invnum => $self->__items_previous_map_invoice($_)
+ } @open_invoices;
+
+ # Which credits and payments displayed on the bill will vary based on
+ # conf flag previous_balance-payments_since.
+ my @credits = $self->_items_credits();
+ my @payments = $self->_items_payments();
+
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ # For each credit or payment, determine which invoices it was applied to.
+ # Manipulate data displayed so the invoice displayed appears as a
+ # snapshot in time... with previous balances and balance owed displayed
+ # as they were at the time of invoice creation.
+
+ my @credits_postbill = $self->_items_credits_postbill();
+ my @payments_postbill = $self->_items_payments_postbill();
+
+ my %pmnt_dupechk;
+ my %cred_dupechk;
+
+ # Each section below follows this pattern on a payment/credit
+ #
+ # - Dupe check, avoid adjusting for the same item twice
+ # - If invoice being adjusted for isn't in our list, add it
+ # - Adjust the invoice balance to refelct balnace without the
+ # credit or payment applied
+ #
+
+ # Working with payments displayed on this bill
+ for my $pmt_hash (@payments) {
+ my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
+ for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
+ next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
+ $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
+
+ my $invnum = $cust_bill_pay->invnum;
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
+ }
+ }
+
+ # Working with credits displayed on this bill
+ for my $cred_hash (@credits) {
+ my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
+ for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
+ next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
+ $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
+
+ my $invnum = $cust_credit_bill->invnum;
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
+ }
+ }
+
+ # Working with both credits and payments which are not displayed
+ # on this bill, but which have affected this bill's balances
+ for my $postbill (@payments_postbill, @credits_postbill) {
+
+ if ($postbill->{billpaynum}) {
+ next if exists $pmnt_dupechk{$postbill->{billpaynum}};
+ $pmnt_dupechk{$postbill->{billpaynum}} = 1;
+ } elsif ($postbill->{creditbillnum}) {
+ next if exists $cred_dupechk{$postbill->{creditbillnum}};
+ $cred_dupechk{$postbill->{creditbillnum}} = 1;
+ } else {
+ die "Missing creditbillnum or billpaynum";
+ }
+
+ my $invnum = $postbill->{invnum};
+
+ $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+ unless exists $invoices{$invnum};
+
+ $invoices{$invnum}->{amount} += $postbill->{amount};
+ }
+
+ # Make sure current invoice doesn't appear in previous items
+ delete $invoices{$self->invnum}
+ if exists $invoices{$self->invnum};
+
+ }
+
+ # Make sure amount is formatted as a dollar string
+ # (Formatting should happen on the template side, but is not?)
+ $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
+ for keys %invoices;
+
+ $self->set('_items_previous', \%invoices);
+ return sort { $a->{_date} <=> $b->{_date} } values %invoices;
+
+}
+
+=item _items_previous_total
+
+ Return sum of amounts from all items returned by _items_previous
+ Results will vary based on invoicing conf flags
+
+=cut
+
+sub _items_previous_total {
+ my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_previous();
+ return $tot;
+}
+
+sub __items_previous_get_invoice {
+ # Helper function for _items_previous
+ #
+ # Read a record from cust_bill, return a hash of it's information
+ my ($self, $invnum) = @_;
+ die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
+
+ my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
+ return $self->__items_previous_map_invoice($cust_bill);
+}
+
+sub __items_previous_map_invoice {
+ # Helper function for _items_previous
+ #
+ # Transform a cust_bill object into a simple hash reference of the type
+ # required by _items_previous
+ my ($self, $cust_bill) = @_;
+ die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
+
+ my $date = $self->conf->exists('invoice_show_prior_due_date')
+ ? 'due '.$cust_bill->due_date2str('short')
+ : $self->time2str_local('short', $cust_bill->_date);
+
+ return {
+ invnum => $cust_bill->invnum,
+ amount => $cust_bill->owed,
+ pkgnum => 'N/A',
+ _date => $cust_bill->_date,
+ description => join(' ',
+ $self->mt('Previous Balance, Invoice #'),
+ $cust_bill->invnum,
+ "($date)"
+ ),
+ }
+}
+
+=item _items_credits()
+
+ Return array of hashrefs containing credits to be shown as line-items
+ when rendering this bill.
+
+ keys for each credit item:
+ - crednum: id of payment
+ - amount: payment amount
+ - description: line item to be displayed on the bill
+
+ This method has three ways it selects which credits to display on
+ this bill:
+
+ 1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+ Returns credits that have been applied to this bill only
+
+ 2) Case:
+ Conf flag set for 'previous_balance-payments_since'
+
+ List all credits that have been recorded during the time period
+ between the timestamps of the last invoice and this invoice
+
+ 3) Case:
+ Conf flag set for 'previous_balance-payments_since'
+ $opt{'template'} eq 'statement'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and the following invoice.
+
+ This is used to give the customer a receipt for a payment
+ in the form of their last bill with the payment amended.
+
+ I am concerned with this implementation, but leaving in place as is
+ If this option is selected, while viewing an older bill, the old bill
+ will show ALL future credits for future bills, but no charges for
+ future bills. Somebody could be misled into believing they have a
+ large account credit when they don't. Also, interrupts the chain of
+ invoices as an account history... the customer could have two invoices
+ in their fileing cabinet, for two different dates, both with a line item
+ for the same duplicate credit. The accounting is technically accurate,
+ but somebody could easily become confused and think two credits were
+ made, when really those two line items on two different bills represent
+ only a single credit
+
+=cut
+
+sub _items_credits {
+
+ my $self= shift;
+
+ # Simple memoize
+ return @{$self->get('_items_credits')} if $self->get('_items_credits');
+
+ my %opt = @_;
+ my $template = $opt{template} || $self->get('_template');
+ my $trim_len = $opt{template} || $self->get('trim_len') || 40;
+
+ my @return;
+ my @cust_credit_objs;
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ if ($template eq 'statement') {
+ # Case 3 (see above)
+ # Return credits timestamped between the previous and following bills
+
+ my $previous_bill = $self->previous_bill;
+ my $following_bill = $self->following_bill;
+
+ my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
+ my $date_end = ref $following_bill ? $following_bill->_date : undef;
+
+ my %query = (
+ table => 'cust_credit',
+ hashref => {
+ custnum => $self->custnum,
+ _date => { op => '>=', value => $date_start },
+ },
+ );
+ $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+ @cust_credit_objs = qsearch(\%query);
+
+ } else {
+ # Case 2 (see above)
+ # Return credits timestamps between this and the previous bills
+
+ my $date_start = 0;
+ my $date_end = $self->_date;
+
+ my $previous_bill = $self->previous_bill;
+ if (ref $previous_bill) {
+ $date_start = $previous_bill->_date;
+ }
+
+ @cust_credit_objs = qsearch({
+ table => 'cust_credit',
+ hashref => {
+ custnum => $self->custnum,
+ _date => {op => '>=', value => $date_start},
+ },
+ extra_sql => " AND _date <= $date_end ",
+ });
+ }
+
+ } else {
+ # Case 1 (see above)
+ # Return only credits that have been applied to this bill
+
+ @cust_credit_objs = $self->cust_credited;
+
+ }
+
+ # Translate objects into hashrefs
+ foreach my $obj ( @cust_credit_objs ) {
+ my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
+ my %r_obj = (
+ amount => sprintf('%.2f',$cust_credit->amount),
+ crednum => $cust_credit->crednum,
+ _date => $cust_credit->_date,
+ creditreason => $cust_credit->reason,
+ );
+
+ my $reason = substr($cust_credit->reason, 0, $trim_len);
+ $reason .= '...' if length($reason) < length($cust_credit->reason);
+ $reason = "($reason)" if $reason;
+
+ $r_obj{description} = join(' ',
+ $self->mt('Credit applied'),
+ $self->time2str_local('short', $cust_credit->_date),
+ $reason,
+ );
+
+ push @return, \%r_obj;
}
+ $self->set('_items_credits',\@return);
+ @return;
+}
+
+=item _items_credits_total
+
+ Return the total of al items from _items_credits
+ Will vary based on invoice display conf flag
- @b;
+=cut
+sub _items_credits_total {
+ my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_credits();
+ return $tot;
}
+
+
+=item _items_credits_postbill()
+
+ Returns an array of hashrefs for credits where
+ - Credit issued after this invoice
+ - Credit applied to an invoice before this invoice
+
+ Returned hashrefs are of the format returned by _items_credits()
+
+=cut
+
+sub _items_credits_postbill {
+ my $self = shift;
+
+ my @cust_credit_bill = qsearch({
+ table => 'cust_credit_bill',
+ select => join(', ',qw(
+ cust_credit_bill.creditbillnum
+ cust_credit_bill._date
+ cust_credit_bill.invnum
+ cust_credit_bill.amount
+ )),
+ addl_from => ' LEFT JOIN cust_credit'.
+ ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
+ extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum.
+ ' AND cust_credit_bill._date > '.$self->_date.
+ ' AND cust_credit_bill.invnum < '.$self->invnum.' ',
+#! did not investigate why hashref doesn't work for this join query
+# hashref => {
+# 'cust_credit.custnum' => {op => '=', value => $self->custnum},
+# 'cust_credit_bill._date' => {op => '>', value => $self->_date},
+# 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
+# },
+ });
+
+ return map {{
+ _date => $_->_date,
+ invnum => $_->invnum,
+ amount => $_->amount,
+ creditbillnum => $_->creditbillnum,
+ }} @cust_credit_bill;
+}
+
+=item _items_payments_postbill()
+
+ Returns an array of hashrefs for payments where
+ - Payment occured after this invoice
+ - Payment applied to an invoice before this invoice
+
+ Returned hashrefs are of the format returned by _items_payments()
+
+=cut
+
+sub _items_payments_postbill {
+ my $self = shift;
+
+ my @cust_bill_pay = qsearch({
+ table => 'cust_bill_pay',
+ select => join(', ',qw(
+ cust_bill_pay.billpaynum
+ cust_bill_pay._date
+ cust_bill_pay.invnum
+ cust_bill_pay.amount
+ )),
+ addl_from => ' LEFT JOIN cust_bill'.
+ ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
+ extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum.
+ ' AND cust_bill_pay._date > '.$self->_date.
+ ' AND cust_bill_pay.invnum < '.$self->invnum.' ',
+ });
+
+ return map {{
+ _date => $_->_date,
+ invnum => $_->invnum,
+ amount => $_->amount,
+ billpaynum => $_->billpaynum,
+ }} @cust_bill_pay;
+}
+
+=item _items_payments()
+
+ Return array of hashrefs containing payments to be shown as line-items
+ when rendering this bill.
+
+ keys for each payment item:
+ - paynum: id of payment
+ - amount: payment amount
+ - description: line item to be displayed on the bill
+
+ This method has three ways it selects which payments to display on
+ this bill:
+
+ 1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+ Returns payments that have been applied to this bill only
+
+ 2) Case:
+ Conf flag set for 'previous_balance-payments_since'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and this invoice
+
+ 3) Case:
+ Conf flag set for 'previous_balance-payments_since'
+ $opt{'template'} eq 'statement'
+
+ List all payments that have been recorded between the timestamps
+ of the previous invoice and the following invoice.
+
+ I am concerned with this implementation, but leaving in place as is
+ If this option is selected, while viewing an older bill, the old bill
+ will show ALL future payments for future bills, but no charges for
+ future bills. Somebody could be misled into believing they have a
+ large account credit when they don't. Also, interrupts the chain of
+ invoices as an account history... the customer could have two invoices
+ in their fileing cabinet, for two different dates, both with a line item
+ for the same duplicate payment. The accounting is technically accurate,
+ but somebody could easily become confused and think two payments were
+ made, when really those two line items on two different bills represent
+ only a single payment.
+
+=cut
+
sub _items_payments {
+
+ my $self = shift;
+
+ # Simple memoize
+ return @{$self->get('_items_payments')} if $self->get('_items_payments');
+
+ my %opt = @_;
+ my $template = $opt{template} || $self->get('_template');
+
+ my @return;
+ my @cust_pay_objs;
+
+ my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+
+ if ($self->conf->exists('previous_balance-payments_since')) {
+ if ($template eq 'statement') {
+ # Case 3 (see above)
+ # Return payments timestamped between the previous and following bills
+
+ my $previous_bill = $self->previous_bill;
+ my $following_bill = $self->following_bill;
+
+ my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
+ my $date_end = ref $following_bill ? $following_bill->_date : undef;
+
+ my %query = (
+ table => 'cust_pay',
+ hashref => {
+ custnum => $self->custnum,
+ _date => { op => '>=', value => $date_start },
+ },
+ );
+ $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+ @cust_pay_objs = qsearch(\%query);
+
+ } else {
+ # Case 2 (see above)
+ # Return payments timestamped between this and the previous bill
+
+ my $date_start = 0;
+ my $date_end = $self->_date;
+
+ my $previous_bill = $self->previous_bill;
+ if (ref $previous_bill) {
+ $date_start = $previous_bill->_date;
+ }
+
+ @cust_pay_objs = qsearch({
+ table => 'cust_pay',
+ hashref => {
+ custnum => $self->custnum,
+ _date => {op => '>=', value => $date_start},
+ },
+ extra_sql => " AND _date <= $date_end ",
+ });
+ }
+
+ } else {
+ # Case 1 (see above)
+ # Return payments applied only to this bill
+
+ @cust_pay_objs = $self->cust_bill_pay;
+
+ }
+
+ $self->set(
+ '_items_payments',
+ [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+ );
+ return @{ $self->get('_items_payments') };
+}
+
+=item _items_payments_total
+
+ Return a total of all records returned by _items_payments
+ Results vary based on invoicing conf flags
+
+=cut
+
+sub _items_payments_total {
my $self = shift;
+ my $tot = 0;
+ $tot += $_->{amount} for $self->_items_payments();
+ return $tot;
+}
+
+sub __items_payments_make_hashref {
+ # Transform a FS::cust_pay object into a simple hashref for invoice
+ my ($self, @cust_pay_objs) = @_;
+ my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+ my @return;
+
+ for my $obj (@cust_pay_objs) {
+
+ # In case we're passed FS::cust_bill_pay (or something else?)
+ # Below, we use $obj to render amount rather than $cust_apy.
+ # If we were passed cust_bill_pay objs, then:
+ # $obj->amount represents the amount applied to THIS invoice
+ # $cust_pay->amount represents the total payment, which may have
+ # been applied accross several invoices.
+ # If we were passed cust_bill_pay objects, then the conf flag
+ # previous_balance-payments_since is NOT set, so we should not
+ # present any payments not applied to this invoice.
+ my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
+
+ my %r_obj = (
+ _date => $cust_pay->_date,
+ amount => sprintf("%.2f", $obj->amount),
+ paynum => $cust_pay->paynum,
+ payinfo => $cust_pay->payby_payinfo_pretty(),
+ description => join(' ',
+ $self->mt('Payment received'),
+ $self->time2str_local('short', $cust_pay->_date),
+ ),
+ );
+
+ if ($c_invoice_payment_details) {
+ $r_obj{description} = join(' ',
+ $r_obj{description},
+ $self->mt('via'),
+ $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
+ );
+ }
+
+ push @return, \%r_obj;
+ }
+ return @return;
+}
- my @b;
- #get & print payments
- foreach ( $self->cust_bill_pay ) {
+=item _items_total()
- #something more elaborate if $_->amount ne ->cust_pay->paid ?
+ Generate the line-items to be shown on the bill in the "Totals" section
+
+ Returns a list of hashrefs, each with the keys:
+ - total_item: description field
+ - total_amount: dollar-formatted number amount
+
+ Information presented by this method varies based on Conf
+
+ Conf previous_balance-payments_due
+ - default, flag not set
+ Only transactions that were applied to this bill bill be
+ displayed and calculated intothe total. If items exist in
+ the past-due section, those items will disappear from this
+ invoice if they have been paid off.
+
+ - previous_balance-payments_due flag is set
+ Transactions occuring after the timestsamp of this
+ invoice are not reflected on invoice line items
+
+ Only payments/credits applied between the previous invoice
+ and this one are displayed and calculated into the total
+
+ - previous_balance-payments_due && $opt{template} eq 'statement'
+ Same as above, except payments/credits occuring before the date
+ of the following invoice are also displayed and calculated into
+ the total
+
+ Conf previous_balance-exclude_from_total
+ - default, flag not set
+ The "Totals" section contains a single line item.
+ The dollar amount of this line items is a sum of old and new charges
+ - previous_balance-exclude_from_total flag is set
+ The "Totals" section contains two line items.
+ One for previous balance, one for new charges
+ !NOTE: Avent virtualization flag 'disable_previous_balance' can
+ override the global conf flag previous_balance-exclude_from_total
+
+ Conf invoice_show_prior_due_date
+ - default, flag not set
+ Total line item in the "Totals" section does not mention due date
+ - invoice_show_prior_due_date flag is set
+ Total line item in the "Totals" section includes either the due
+ date of the invoice, or the specified invoice terms
+ ? Not sure why this is called "Prior" due date, since we seem to be
+ displaying THIS due date...
+=cut
- push @b, {
- 'description' => $self->mt('Payment received').' '.
- time2str($date_format,$_->cust_pay->_date ),
- 'amount' => sprintf("%.2f", $_->amount )
+sub _items_total {
+ my $self = shift;
+ my $conf = $self->conf;
+
+ my $c_multi_line_total = 0;
+ $c_multi_line_total = 1
+ if $conf->exists('previous_balance-exclude_from_total')
+ && $self->enable_previous();
+
+ my @line_items;
+ my $invoice_charges = $self->charged();
+
+ # _items_previous() is aware of conf flags
+ my $previous_balance = 0;
+ $previous_balance += $_->{amount} for $self->_items_previous();
+
+ my $total_charges;
+ my $total_descr;
+
+ if ( $previous_balance && $c_multi_line_total ) {
+ # previous balance, new charges on separate lines
+
+ push @line_items, {
+ total_amount => sprintf('%.2f',$previous_balance),
+ total_item => $self->mt(
+ $conf->config('previous_balance-text') || 'Previous Balance'
+ ),
};
+
+ $total_charges = $invoice_charges;
+ $total_descr = $self->mt(
+ $conf->config('previous_balance-text-total_new_charges')
+ || 'Total New Charges'
+ );
+
+ } else {
+ # previous balance and new charges combined into a single total line
+ $total_charges = $invoice_charges + $previous_balance;
+ $total_descr = $self->mt('Total Charges');
}
- @b;
+ if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
+ # then the due date should be shown with Total New Charges,
+ # and should NOT be shown with the Balance Due message.
+
+ if ( $self->due_date ) {
+ $total_descr .= $self->invoice_pay_by_msg;
+ } elsif ( $self->terms ) {
+ $total_descr = join(' ',
+ $total_descr,
+ '-',
+ $self->mt($self->terms)
+ );
+ }
+ }
+ push @line_items, {
+ total_amount => sprintf('%.2f', $total_charges),
+ total_item => $total_descr,
+ };
+
+ return @line_items;
+}
+
+=item _items_aging_balances
+
+ Returns an array of aged balance amounts from a given epoch timestamp.
+
+ The time of day is ignored for this calculation, so that slight differences
+ on the generation time of an invoice doesn't determine which column an
+ aged balance falls into.
+
+ Will not include any balances dated after the given timestamp in
+ the calculated totals
+
+ usage:
+ @aged_balances = $b->_items_aging_balances( $b->_date )
+
+ @aged_balances = (
+ under30d,
+ 30d-60d,
+ 60d-90d,
+ over90d
+ )
+
+=cut
+
+sub _items_aging_balances {
+ my ($self, $basetime) = @_;
+ die "Incorrect usage of _items_aging_balances()" unless ref $self;
+
+ $basetime = $self->_date unless $basetime;
+ my @aging_balances = (0, 0, 0, 0);
+ my @open_invoices = $self->_items_previous();
+ my $d30 = 2592000; # 60 * 60 * 24 * 30,
+ my $d60 = 5184000; # 60 * 60 * 24 * 60,
+ my $d90 = 7776000; # 60 * 60 * 24 * 90
+
+ # Move the clock back on our given day to 12:00:01 AM
+ my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
+ my $dt_12am = DateTime->new(
+ year => $dt_basetime->year,
+ month => $dt_basetime->month,
+ day => $dt_basetime->day,
+ hour => 0,
+ minute => 0,
+ second => 1,
+ )->epoch();
+
+ # set our epoch breakpoints
+ $_ = $dt_12am - $_ for $d30, $d60, $d90;
+
+ # grep the aged balances
+ for my $oinv (@open_invoices) {
+ if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
+ # If post invoice dated less than 30days ago
+ $aging_balances[0] += $oinv->{amount};
+ } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
+ # If past invoice dated between 30-60 days ago
+ $aging_balances[1] += $oinv->{amount};
+ } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
+ # If past invoice dated between 60-90 days ago
+ $aging_balances[2] += $oinv->{amount};
+ } else {
+ # If past invoice dated 90+ days ago
+ $aging_balances[3] += $oinv->{amount};
+ }
+ }
+
+ return map{ sprintf('%.2f',$_) } @aging_balances;
+}
+
+=item has_call_details
+
+Returns true if this invoice has call details.
+
+=cut
+
+sub has_call_details {
+ my $self = shift;
+ $self->scalar_sql("
+ SELECT 1 FROM cust_bill_pkg_detail
+ LEFT JOIN cust_bill_pkg USING (billpkgnum)
+ WHERE cust_bill_pkg_detail.format = 'C'
+ AND cust_bill_pkg.invnum = ?
+ LIMIT 1
+ ", $self->invnum);
}
=item call_details [ OPTION => VALUE ... ]
my $row = shift;
$row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
-
+
};
}
( $header, grep { $_ ne $header } @details );
}
+=item cust_pay_batch
+
+Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
+will be removed.
+
+=cut
+
+sub cust_pay_batch {
+ carp "FS::cust_bill->cust_pay_batch is deprecated";
+ my $self = shift;
+ qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
+}
=back
process_re_X('spool', @_);
}
-use Storable qw(thaw);
use Data::Dumper;
-use MIME::Base64;
sub process_re_X {
my( $method, $job ) = ( shift, shift );
warn "$me process_re_X $method for job $job\n" if $DEBUG;
- my $param = thaw(decode_base64(shift));
+ my $param = shift;
warn Dumper($param) if $DEBUG;
re_X(
}
+# this is called from search/cust_bill.html and given all its search
+# parameters, so it needs to perform the same search.
+
sub re_X {
# spool_invoice ftp_invoice fax_invoice print_invoice
my($method, $job, %param ) = @_;
}
#some false laziness w/search/cust_bill.html
- my $distinct = '';
- my $orderby = 'ORDER BY cust_bill._date';
-
- my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
-
- my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
-
- my @cust_bill = qsearch( {
- #'select' => "cust_bill.*",
- 'table' => 'cust_bill',
- 'addl_from' => $addl_from,
- 'hashref' => {},
- 'extra_sql' => $extra_sql,
- 'order_by' => $orderby,
- 'debug' => 1,
- } );
+ $param{'order_by'} = 'cust_bill._date';
+
+ my $query = FS::cust_bill->search(\%param);
+ delete $query->{'count_query'};
+ delete $query->{'count_addl'};
+
+ $query->{debug} = 1; # was in here before, is obviously useful
+
+ my @cust_bill = qsearch( $query );
$method .= '_invoice' unless $method eq 'email' || $method eq 'print';
}
+sub API_getinfo {
+ my $self = shift;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+ 'owed' => $self->owed,
+ #XXX last payment applied date
+ };
+}
+
=back
=head1 CLASS METHODS
sub owed_sql {
my ($class, $start, $end) = @_;
- 'charged - '.
- $class->paid_sql($start, $end). ' - '.
+ 'charged - '.
+ $class->paid_sql($start, $end). ' - '.
$class->credited_sql($start, $end);
}
=cut
sub due_date_sql {
+ die "don't use: doesn't account for agent-specific invoice_default_terms";
+
+ #we're passed a $conf but not a specific customer (that's in the query), so
+ # to make this work we'd need an agentnum-aware "condition_sql_conf" like
+ # "condition_sql_option" that retreives a conf value with SQL in an agent-
+ # aware fashion
+
my $conf = new FS::Conf;
'COALESCE(
SUBSTRING(
) * 86400 + cust_bill._date'
}
-=item search_sql_where HASHREF
-
-Class method which returns an SQL WHERE fragment to search for parameters
-specified in HASHREF. Valid parameters are
-
-=over 4
-
-=item _date
-
-List reference of start date, end date, as UNIX timestamps.
-
-=item invnum_min
-
-=item invnum_max
-
-=item agentnum
-
-=item charged
-
-List reference of charged limits (exclusive).
-
-=item owed
-
-List reference of charged limits (exclusive).
-
-=item open
-
-flag, return open invoices only
-
-=item net
-
-flag, return net invoices only
-
-=item days
-
-=item newest_percust
-
-=back
-
-Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
-
-=cut
-
-sub search_sql_where {
- my($class, $param) = @_;
- if ( $DEBUG ) {
- warn "$me search_sql_where called with params: \n".
- join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
- }
-
- my @search = ();
-
- #agentnum
- if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.agentnum = $1";
- }
-
- #refnum
- if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.refnum = $1";
- }
-
- #custnum
- if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill.custnum = $1";
- }
-
- #_date
- if ( $param->{_date} ) {
- my($beginning, $ending) = @{$param->{_date}};
-
- push @search, "cust_bill._date >= $beginning",
- "cust_bill._date < $ending";
- }
-
- #invnum
- if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill.invnum >= $1";
- }
- if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill.invnum <= $1";
- }
-
- #charged
- if ( $param->{charged} ) {
- my @charged = ref($param->{charged})
- ? @{ $param->{charged} }
- : ($param->{charged});
-
- push @search, map { s/^charged/cust_bill.charged/; $_; }
- @charged;
- }
-
- my $owed_sql = FS::cust_bill->owed_sql;
-
- #owed
- if ( $param->{owed} ) {
- my @owed = ref($param->{owed})
- ? @{ $param->{owed} }
- : ($param->{owed});
- push @search, map { s/^owed/$owed_sql/; $_; }
- @owed;
- }
-
- #open/net flags
- push @search, "0 != $owed_sql"
- if $param->{'open'};
- push @search, '0 != '. FS::cust_bill->net_sql
- if $param->{'net'};
-
- #days
- push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
- if $param->{'days'};
-
- #newest_percust
- if ( $param->{'newest_percust'} ) {
-
- #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
- #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
-
- my @newest_where = map { my $x = $_;
- $x =~ s/\bcust_bill\./newest_cust_bill./g;
- $x;
- }
- grep ! /^cust_main./, @search;
- my $newest_where = scalar(@newest_where)
- ? ' AND '. join(' AND ', @newest_where)
- : '';
-
-
- push @search, "cust_bill._date = (
- SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
- WHERE newest_cust_bill.custnum = cust_bill.custnum
- $newest_where
- )";
-
- }
-
- #promised_date - also has an option to accept nulls
- if ( $param->{promised_date} ) {
- my($beginning, $ending, $null) = @{$param->{promised_date}};
-
- push @search, "(( cust_bill.promised_date >= $beginning AND ".
- "cust_bill.promised_date < $ending )" .
- ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
- }
-
- #agent virtualization
- my $curuser = $FS::CurrentUser::CurrentUser;
- if ( $curuser->username eq 'fs_queue'
- && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
- my $username = $1;
- my $newuser = qsearchs('access_user', {
- 'username' => $username,
- 'disabled' => '',
- } );
- if ( $newuser ) {
- $curuser = $newuser;
- } else {
- warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
- }
- }
- push @search, $curuser->agentnums_sql;
-
- join(' AND ', @search );
-
-}
-
=back
=head1 BUGS
=cut
1;
-