package FS::cust_bill;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
-use vars qw( $invoice_lines @buf ); #yuck
+use vars qw( $DEBUG $me $date_format );
+ # but NOT $conf
use Fcntl qw(:flock); #for spool_csv
-use List::Util qw(min max);
+use Cwd;
+use List::Util qw(min max sum);
use Date::Format;
-use Text::Template 1.20;
use File::Temp 0.14;
-use String::ShellQuote;
use HTML::Entities;
-use Locale::Country;
+use Storable qw( freeze thaw );
+use GD::Barcode;
use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
+use FS::Misc qw( send_email send_fax do_print );
use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main_Mixin;
use FS::cust_main;
+use FS::cust_statement;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_detail;
use FS::cust_credit;
use FS::cust_pay;
use FS::cust_pkg;
use FS::cust_bill_pay_batch;
use FS::part_bill_event;
use FS::payby;
-
-@ISA = qw( FS::cust_main_Mixin FS::Record );
+use FS::bill_batch;
+use FS::cust_bill_batch;
+use FS::cust_bill_pay_pkg;
+use FS::cust_credit_bill_pkg;
+use FS::discount_plan;
+use FS::cust_bill_void;
+use FS::L10N;
$DEBUG = 0;
$me = '[FS::cust_bill]';
#ask FS::UID to run this stuff for us later
FS::UID->install_callback( sub {
- $conf = new FS::Conf;
- $money_char = $conf->config('money_char') || '$';
+ my $conf = new FS::Conf; #global
+ $date_format = $conf->config('date_format') || '%x'; #/YY
} );
=head1 NAME
(see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
following fields are currently supported:
+Regular fields
+
=over 4
=item invnum - primary key (assigned automatically for new invoices)
=item charged - amount of this invoice
+=item invoice_terms - optional terms override for this specific invoice
+
+=back
+
+Customer info at invoice generation time
+
+=over 4
+
+=item billing_balance - the customer's balance at the time the invoice was
+generated (not including charges on this invoice)
+
+=item previous_balance - the billing_balance of this customer's previous
+invoice plus the charges on that invoice
+
+=back
+
+Deprecated
+
+=over 4
+
=item printed - deprecated
+=back
+
+Specific use cases
+
+=over 4
+
=item closed - books closed flag, empty or `Y'
+=item statementnum - invoice aggregation (see L<FS::cust_statement>)
+
+=item agent_invid - legacy invoice number
+
+=item promised_date - customer promised payment date, for collection
+
=back
=head1 METHODS
=cut
sub table { 'cust_bill'; }
+sub notice_name { 'Invoice'; }
sub cust_linked { $_[0]->cust_main_custnum; }
sub cust_unlinked_msg {
Adds this invoice to the database ("Posts" the invoice). If there is an error,
returns the error, otherwise returns false.
+=cut
+
+sub insert {
+ my $self = shift;
+ warn "$me insert called\n" if $DEBUG;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error = $self->SUPER::insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $self->get('cust_bill_pkg') ) {
+ foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
+ $cust_bill_pkg->invnum($self->invnum);
+ my $error = $cust_bill_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't create invoice line item: $error";
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item void
+
+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
+FS::cust_bill_pkg_void).
+
+=cut
+
+sub void {
+ my $self = shift;
+ my $reason = scalar(@_) ? shift : '';
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_bill_void = new FS::cust_bill_void ( {
+ map { $_ => $self->get($_) } $self->fields
+ } );
+ $cust_bill_void->reason($reason);
+ my $error = $cust_bill_void->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ my $error = $cust_bill_pkg->void($reason);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
+}
+
=item delete
This method now works but you probably shouldn't use it. Instead, apply a
-credit against the invoice.
+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
sub delete {
my $self = shift;
return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
- $self->SUPER::delete(@_);
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ 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_batch
+ cust_bill_pkg
+ )) {
+
+ foreach my $linked ( $self->$table() ) {
+ my $error = $linked->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ my $error = $self->SUPER::delete(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
}
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ]
-Replaces the OLD_RECORD with this one in the database. If there is an error,
-returns the error, otherwise returns false.
+You can, but probably shouldn't modify invoices...
-Only printed may be changed. printed is normally updated by calling the
-collect method of a customer object (see L<FS::cust_main>).
+Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
+supplied, replaces this record. If there is an error, returns the error,
+otherwise returns false.
=cut
sub replace_check {
my( $new, $old ) = ( shift, shift );
- return "Can't change custnum!" unless $old->custnum == $new->custnum;
+ return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
#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->charged == 0;
+ return "Can't change _date" unless $old->_date == $new->_date;
+ return "Can't change charged" unless $old->charged == $new->charged
+ || $old->charged == 0
+ || $new->{'Hash'}{'cc_surcharge_replace_hack'};
'';
}
+
+=item add_cc_surcharge
+
+Giant hack
+
+=cut
+
+sub add_cc_surcharge {
+ my ($self, $pkgnum, $amount) = (shift, shift, shift);
+
+ my $error;
+ my $cust_bill_pkg = new FS::cust_bill_pkg({
+ 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ 'setup' => $amount,
+ });
+ $error = $cust_bill_pkg->insert;
+ return $error if $error;
+
+ $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
+ $self->charged($self->charged+$amount);
+ $error = $self->replace;
+ return $error if $error;
+
+ $self->apply_payments_and_credits;
+}
+
+
=item check
Checks all fields to make sure this is a valid invoice. If there is an error,
my $error =
$self->ut_numbern('invnum')
- || $self->ut_number('custnum')
+ || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
|| $self->ut_numbern('_date')
|| $self->ut_money('charged')
|| $self->ut_numbern('printed')
|| $self->ut_enum('closed', [ '', 'Y' ])
+ || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
+ || $self->ut_numbern('agent_invid') #varchar?
;
return $error if $error;
- return "Unknown customer"
- unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-
$self->_date(time) unless $self->_date;
$self->printed(0) if $self->printed eq '';
$self->SUPER::check;
}
+=item display_invnum
+
+Returns the displayed invoice number for this invoice: agent_invid if
+cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
+
+=cut
+
+sub display_invnum {
+ my $self = shift;
+ my $conf = $self->conf;
+ if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ return $self->agent_invid;
+ } else {
+ return $self->invnum;
+ }
+}
+
=item previous
Returns a list consisting of the total previous balance for this customer,
my $self = shift;
my $total = 0;
my @cust_bill = sort { $a->_date <=> $b->_date }
- grep { $_->owed != 0 && $_->_date < $self->_date }
- qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
+ grep { $_->owed != 0 }
+ qsearch( 'cust_bill', { 'custnum' => $self->custnum,
+ #'_date' => { op=>'<', value=>$self->_date },
+ 'invnum' => { op=>'<', value=>$self->invnum },
+ } )
;
foreach ( @cust_bill ) { $total += $_->owed; }
$total, @cust_bill;
}
+=item enable_previous
+
+Whether to show the 'Previous Charges' section when printing this invoice.
+The negation of the 'disable_previous_balance' config setting.
+
+=cut
+
+sub enable_previous {
+ my $self = shift;
+ my $agentnum = $self->cust_main->agentnum;
+ !$self->conf->exists('disable_previous_balance', $agentnum);
+}
+
=item cust_bill_pkg
Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
);
}
+=item cust_bill_pkg_pkgnum PKGNUM
+
+Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
+specified pkgnum.
+
+=cut
+
+sub cust_bill_pkg_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ qsearch(
+ { 'table' => 'cust_bill_pkg',
+ 'hashref' => { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ },
+ 'order_by' => 'ORDER BY billpkgnum',
+ }
+ );
+}
+
=item cust_pkg
Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
sub cust_pkg {
my $self = shift;
- my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+ my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+ $self->cust_bill_pkg;
my %saw = ();
grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
}
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+ my $self = shift;
+ grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
=item open_cust_bill_pkg
Returns the open line items for this invoice.
#;
}
+sub cust_pay_batch {
+ my $self = shift;
+ qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
+}
+
+sub cust_bill_pay_batch {
+ my $self = shift;
+ qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+}
+
=item cust_bill_pay
Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
sub cust_bill_pay {
my $self = shift;
+ map { $_ } #return $self->num_cust_bill_pay unless wantarray;
sort { $a->_date <=> $b->_date }
qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
}
=item cust_credited
+=item cust_credit_bill
+
Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
=cut
sub cust_credited {
my $self = shift;
+ map { $_ } #return $self->num_cust_credit_bill unless wantarray;
sort { $a->_date <=> $b->_date }
qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
;
}
+sub cust_credit_bill {
+ shift->cust_credited(@_);
+}
+
+#=item cust_bill_pay_pkgnum PKGNUM
+#
+#Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_bill_pay_pkgnum {
+# my( $self, $pkgnum ) = @_;
+# map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
+# sort { $a->_date <=> $b->_date }
+# qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+# 'pkgnum' => $pkgnum,
+# }
+# );
+#}
+
+=item cust_bill_pay_pkg PKGNUM
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+applied against the matching pkgnum.
+
+=cut
+
+sub cust_bill_pay_pkg {
+ my( $self, $pkgnum ) = @_;
+
+ qsearch({
+ 'select' => 'cust_bill_pay_pkg.*',
+ 'table' => 'cust_bill_pay_pkg',
+ 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
+ ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
+ 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+ " AND cust_bill_pkg.pkgnum = $pkgnum",
+ });
+
+}
+
+#=item cust_credited_pkgnum PKGNUM
+#
+#=item cust_credit_bill_pkgnum PKGNUM
+#
+#Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_credited_pkgnum {
+# my( $self, $pkgnum ) = @_;
+# map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
+# sort { $a->_date <=> $b->_date }
+# qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+# 'pkgnum' => $pkgnum,
+# }
+# );
+#}
+#
+#sub cust_credit_bill_pkgnum {
+# shift->cust_credited_pkgnum(@_);
+#}
+
+=item cust_credit_bill_pkg PKGNUM
+
+Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
+applied against the matching pkgnum.
+
+=cut
+
+sub cust_credit_bill_pkg {
+ my( $self, $pkgnum ) = @_;
+
+ qsearch({
+ 'select' => 'cust_credit_bill_pkg.*',
+ 'table' => 'cust_credit_bill_pkg',
+ 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
+ ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
+ 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+ " AND cust_bill_pkg.pkgnum = $pkgnum",
+ });
+
+}
+
+=item cust_bill_batch
+
+Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
+
+=cut
+
+sub cust_bill_batch {
+ my $self = shift;
+ qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
+}
+
+=item discount_plans
+
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
+hash keyed by term length.
+
+=cut
+
+sub discount_plans {
+ my $self = shift;
+ FS::discount_plan->all($self);
+}
+
=item tax
Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
$balance;
}
-=item apply_payments_and_credits
+sub owed_pkgnum {
+ my( $self, $pkgnum ) = @_;
+
+ #my $balance = $self->charged;
+ my $balance = 0;
+ $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
+
+ $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
+ $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
+
+ $balance = sprintf( "%.2f", $balance);
+ $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+ $balance;
+}
+
+=item hide
+
+Returns true if this invoice should be hidden. See the
+selfservice-hide_invoices-taxclass configuraiton setting.
=cut
-sub apply_payments_and_credits {
+sub hide {
my $self = shift;
+ my $conf = $self->conf;
+ my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
+ or return '';
+ my @cust_bill_pkg = $self->cust_bill_pkg;
+ my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
+ ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
+}
+
+=item apply_payments_and_credits [ OPTION => VALUE ... ]
+
+Applies unapplied payments and credits to this invoice.
+
+A hash of optional arguments may be passed. Currently "manual" is supported.
+If true, a payment receipt is sent instead of a statement when
+'payment_receipt_email' configuration option is set.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub apply_payments_and_credits {
+ my( $self, %options ) = @_;
+ my $conf = $self->conf;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
+ if ( $conf->exists('pkg-balances') ) {
+ # limit @payments & @credits to those w/ a pkgnum grepped from $self
+ my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
+ @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
+ @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
+ }
+
while ( $self->owed > 0 and ( @payments || @credits ) ) {
my $app = '';
die "guru meditation #12 and 35";
}
+ my $unapp_amount;
if ( $app eq 'pay' ) {
my $payment = shift @payments;
-
- $app = new FS::cust_bill_pay {
- 'paynum' => $payment->paynum,
- 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
- };
+ $unapp_amount = $payment->unapplied;
+ $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
+ $app->pkgnum( $payment->pkgnum )
+ if $conf->exists('pkg-balances') && $payment->pkgnum;
} elsif ( $app eq 'credit' ) {
my $credit = shift @credits;
-
- $app = new FS::cust_credit_bill {
- 'crednum' => $credit->crednum,
- 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
- };
+ $unapp_amount = $credit->credited;
+ $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
+ $app->pkgnum( $credit->pkgnum )
+ if $conf->exists('pkg-balances') && $credit->pkgnum;
} else {
die "guru meditation #12 and 35";
}
+ my $owed;
+ if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
+ warn "owed_pkgnum ". $app->pkgnum;
+ $owed = $self->owed_pkgnum($app->pkgnum);
+ } else {
+ $owed = $self->owed;
+ }
+ next unless $owed > 0;
+
+ warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
+ $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
+
$app->invnum( $self->invnum );
- my $error = $app->insert;
+ my $error = $app->insert(%options);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error inserting ". $app->table. " record: $error";
email subject, optional
+=item notice_name
+
+notice name instead of "Invoice", optional
+
=back
Returns an argument list to be passed to L<FS::Misc::send_email>.
my $self = shift;
my %args = @_;
+ my $conf = $self->conf;
my $me = '[FS::cust_bill::generate_email]';
'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)$/ }
- $self->cust_main->invoicing_list
+ $cust_main->invoicing_list
];
}
my $alternative = build MIME::Entity
'Type' => 'multipart/alternative',
- 'Encoding' => '7bit',
+ #'Encoding' => '7bit',
'Disposition' => 'inline'
;
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$data = $args{'print_text'};
} else {
- $data = [ $self->print_text('', $args{'template'}) ];
+ $data = [ $self->print_text(\%opt) ];
}
}
$alternative->attach(
'Type' => 'text/plain',
- #'Encoding' => 'quoted-printable',
- 'Encoding' => '7bit',
+ 'Encoding' => 'quoted-printable',
+ #'Encoding' => '7bit',
'Data' => $data,
'Disposition' => 'inline',
);
- $args{'from'} =~ /\@([\w\.\-]+)/;
- my $from = $1 || 'example.com';
- my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-
- my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
- my $file;
- if ( defined($args{'template'}) && length($args{'template'})
- && -e "$path/logo_". $args{'template'}. ".png"
- )
- {
- $file = "$path/logo_". $args{'template'}. ".png";
+
+ my $htmldata;
+ my $image = '';
+ my $barcode = '';
+ if ( $conf->exists('invoice_email_pdf')
+ and scalar($conf->config('invoice_email_pdf_note')) ) {
+
+ $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
+
} else {
- $file = "$path/logo.png";
- }
- my $image = build MIME::Entity
- 'Type' => 'image/png',
- 'Encoding' => 'base64',
- 'Path' => $file,
- 'Filename' => 'logo.png',
- 'Content-ID' => "<$content_id>",
- ;
+ $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 });
+ }
$alternative->attach(
'Type' => 'text/html',
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- $self->print_html('', $args{'template'}, $content_id),
+ $htmldata,
' </body>',
'</html>',
],
#'Filename' => 'invoice.pdf',
);
- if ( $conf->exists('invoice_email_pdf') ) {
- #attaching pdf too:
- # multipart/mixed
+ my @otherparts = ();
+ if ( $cust_main->email_csv_cdr ) {
+
+ 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',
+ ;
+
+ }
+
+ if ( $conf->exists('invoice_email_pdf') ) {
+
+ #attaching pdf too:
+ # multipart/mixed
# multipart/related
# multipart/alternative
# text/plain
$related->add_part($alternative);
- $related->add_part($image);
+ $related->add_part($image) if $image;
- my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+ my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
- $return{'mimeparts'} = [ $related, $pdf ];
+ $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
} else {
# image/png
$return{'content-type'} = 'multipart/related';
- $return{'mimeparts'} = [ $alternative, $image ];
+ 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';
#mime parts arguments a la MIME::Entity->build().
$return{'mimeparts'} = [
- { $self->mimebuild_pdf('', $args{'template'}) }
+ { $self->mimebuild_pdf(\%opt) }
];
}
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$return{'body'} = $args{'print_text'};
} else {
- $return{'body'} = [ $self->print_text('', $args{'template'}) ];
+ $return{'body'} = [ $self->print_text(\%opt) ];
}
}
'Encoding' => 'base64',
'Data' => [ $self->print_pdf(@_) ],
'Disposition' => 'attachment',
- 'Filename' => 'invoice.pdf',
+ 'Filename' => 'invoice-'. $self->invnum. '.pdf',
);
}
-=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
Sends this invoice to the destinations configured for this customer: sends
email, prints and/or faxes. See L<FS::cust_main_invoice>.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to
+four values for templatename, agentnum, invoice_from and amount.
-AGENTNUM, if specified, means that this invoice will only be sent for customers
+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.
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From: address.
-AMOUNT, if specified, only sends the invoice if the total amount owed on this
+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)
+
+I<lpr>, if specified, is passed to
+
=cut
sub queueable_send {
sub send {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
- if ( scalar(@_) && $_[0] ) {
- my $agentnums = ref($_[0]) ? shift : [ shift ];
- return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
+ my $conf = $self->conf;
+
+ my( $template, $invoice_from, $notice_name );
+ my $agentnums = '';
+ my $balance_over = 0;
+ my $lpr = '';
+
+ 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'};
+ $lpr = $opt->{'lpr'}
+ } else {
+ $template = scalar(@_) ? shift : '';
+ if ( scalar(@_) && $_[0] ) {
+ $agentnums = ref($_[0]) ? shift : [ shift ];
+ }
+ $invoice_from = shift if scalar(@_);
+ $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
}
- my $invoice_from =
- scalar(@_)
- ? shift
- : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+ my $cust_main = $self->cust_main;
- my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+ return 'N/A' unless ! $agentnums
+ or grep { $_ == $cust_main->agentnum } @$agentnums;
return ''
- unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
+ unless $cust_main->total_owed_date($self->_date) > $balance_over;
+
+ $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 = $self->cust_main->invoicing_list;
+ my @invoicing_list = $cust_main->invoicing_list;
- #$self->email_invoice($template, $invoice_from)
- $self->email($template, $invoice_from)
- if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
+ #$self->email_invoice(\%opt)
+ $self->email(\%opt)
+ if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+ && ! $self->invoice_noemail;
- #$self->print_invoice($template)
- $self->print($template)
+ $opt{'lpr'} = $lpr;
+ #$self->print_invoice(\%opt)
+ $self->print(\%opt)
if grep { $_ eq 'POST' } @invoicing_list; #postal
- $self->fax_invoice($template)
+ #this has never been used post-$ORIGINAL_ISP afaik
+ $self->fax_invoice(\%opt)
if grep { $_ eq 'FAX' } @invoicing_list; #fax
'';
}
-=item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
+=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
Emails this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+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.
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+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
my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
or die "invalid invoice number: " . $opt{invnum};
- my @args = ( $opt{template} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
+ my %args = ( 'template' => $opt{template} );
+ $args{$_} = $opt{$_}
+ foreach grep { exists($opt{$_}) && $opt{$_} }
+ qw( invoice_from notice_name no_coupon );
- my $error = $self->email( @args );
+ my $error = $self->email( \%args );
die $error if $error;
}
#sub email_invoice {
sub email {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
- my $invoice_from =
- scalar(@_)
- ? shift
- : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+ 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;
- #better to notify this person than silence
- @invoicing_list = ($invoice_from) unless @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 ],
- 'template' => $template,
+ '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;
}
-=item lpr_data [ TEMPLATENAME ]
+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.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+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, $template) = @_;
- $conf->exists('invoice_latex')
- ? [ $self->print_ps('', $template) ]
- : [ $self->print_text('', $template) ];
+ 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 [ TEMPLATENAME ]
+=item print HASHREF | [ TEMPLATE ]
Prints this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+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;
- my $template = scalar(@_) ? shift : '';
+ return if $self->hide;
+ my $conf = $self->conf;
+
+ my( $template, $notice_name, $lpr );
+ if ( ref($_[0]) ) {
+ my $opt = shift;
+ $template = $opt->{'template'} || '';
+ $notice_name = $opt->{'notice_name'} || 'Invoice';
+ $lpr = $opt->{'lpr'}
+ } else {
+ $template = scalar(@_) ? shift : '';
+ $notice_name = 'Invoice';
+ $lpr = '';
+ }
+
+ my %opt = (
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ );
- do_print $self->lpr_data($template);
+ 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 fax_invoice [ TEMPLATENAME ]
+=item fax_invoice HASHREF | [ TEMPLATE ]
Faxes this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+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;
- my $template = scalar(@_) ? 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 $error = send_fax( 'docdata' => $self->lpr_data($template),
+ 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
+
+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
+enabled)
+
+=cut
+
+sub get_open_bill_batch {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $hashref = { status => 'O' };
+ $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
+ ? $self->cust_main->agentnum
+ : '';
+ my $batch = qsearchs('bill_batch', $hashref);
+ return $batch if $batch;
+ $batch = FS::bill_batch->new($hashref);
+ my $error = $batch->insert;
+ die $error if $error;
+ return $batch;
+}
+
=item ftp_invoice [ TEMPLATENAME ]
Sends this invoice data via FTP.
sub ftp_invoice {
my $self = shift;
+ my $conf = $self->conf;
my $template = scalar(@_) ? shift : '';
$self->send_csv(
sub spool_invoice {
my $self = shift;
+ my $conf = $self->conf;
my $template = scalar(@_) ? shift : '';
$self->spool_csv(
=over 4
-=item format - 'default' or 'billco'
+=item format - any of FS::Misc::::Invoicing::spool_formats
+
+=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
+customer has the corresponding invoice destinations set (see
+L<FS::cust_main_invoice>).
-=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
+=item agent_spools - if set to a true value, will spool to per-agent files
+rather than a single global file
-=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
+=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 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
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 = 'agentnum'.$cust_main->agentnum;
+ } else {
+ $file = 'spool';
+ }
- my $file =
- "$spooldir/".
- ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
- ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
- '.csv';
+ if ( $opt{'upload_targetnum'} ) {
+ $spooldir .= '/target'.$opt{'upload_targetnum'};
+ mkdir $spooldir, 0700 unless -d $spooldir;
+ } # otherwise it just goes into export.xxx/cust_bill
+
+ if ( lc($opt{'format'}) eq 'billco' ) {
+ $file .= '-header';
+ }
+
+ $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);
flock(CSV, LOCK_UN);
close CSV;
- $file =
- "$spooldir/".
- ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
- '-detail.csv';
+ $file =~ s/-header.csv$/-detail.csv/;
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;
Options are:
-format - 'default' or 'billco'
+format - 'default', 'billco', 'oneline', 'bridgestone'
Returns a list consisting of two scalars. The first is a single line of CSV
header information for this invoice. The second is one or more lines of 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, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+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
+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, 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
+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
+"once_percust_every" condition.
+
=cut
sub print_csv {
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;
+
+ if ( $format eq 'billco' ) {
my $taxtotal = 0;
$taxtotal += $_->{'amount'} foreach $self->_items_tax;
'0', # 29 | Other Taxes & Fees*** NUM* 9
);
- } else {
+ } 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, #_items_nontax? no sections or anything
+ # with this format
+ $self->_items_tax;
+
+ $csv->combine(
+ $cust_main->agentnum,
+ $cust_main->agent->agent,
+ $self->custnum,
+ $cust_main->first,
+ $cust_main->last,
+ $cust_main->company,
+ $cust_main->address1,
+ $cust_main->address2,
+ $cust_main->city,
+ $cust_main->state,
+ $cust_main->zip,
+
+ # invoice fields
+ time2str("%x", $self->_date),
+ $self->invnum,
+ $self->charged,
+ $totaldue,
+ $previous_balance,
+ $self->due_date2str("%x"),
+
+ @items,
+ );
+
+ } elsif ( $format eq 'bridgestone' ) {
+
+ # bypass the CSV stuff and just return this
+ 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)
+ || '';
+ return (
+ sprintf(
+ "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
+ $prefix,
+ $cust_main->display_custnum,
+ $longdate,
+ uc(substr($cust_main->contact_firstlast,0,30)),
+ uc(substr($cust_main->company ,0,30)),
+ uc(substr($cust_main->address1 ,0,30)),
+ uc(substr($cust_main->address2 ,0,30)),
+ uc(substr($cust_main->city ,0,20)),
+ uc($cust_main->state),
+ $zip
+ ),
+ '' #detail
+ );
+
+ } 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',
}
+ } elsif ( lc($opt{'format'}) eq 'oneline' ) {
+
+ #do nothing
+
} else {
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
} else { #pkgnum tax
next unless $cust_bill_pkg->setup != 0;
- my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
- ? ( $cust_bill_pkg->itemdesc || 'Tax' )
- : 'Tax';
- ($pkg, $setup, $recur, $sdate, $edate) =
- ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+ $pkg = $cust_bill_pkg->desc;
+ $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
+ ( $sdate, $edate ) = ( '', '' );
}
$csv->combine(
}
sub realtime_bop {
- my( $self, $method ) = @_;
+ my( $self, $method ) = (shift,shift);
+ my $conf = $self->conf;
+ my %opt = @_;
my $cust_main = $self->cust_main;
my $balance = $cust_main->balance;
$cust_main->realtime_bop($method, $amount,
'description' => $description,
'invnum' => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+# 'apply' => 1,
+ 'apply_to_invoice' => 1,
+ %opt,
+ #what we want:
+ #this changes application behavior: auto payments
+ #triggered against a specific invoice are now applied
+ #to that invoice instead of oldest open.
+ #seem okay to me...
);
}
$self->cust_main->agent_invoice_from;
}
-=item print_text [ TIME [ , TEMPLATE ] ]
+=item invoice_barcode DIR_OR_FALSE
-Returns an text invoice, as a list of lines.
-
-TIME an optional value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
+Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
+it is taken as the temp directory where the PNG file will be generated and the
+PNG file name is returned. Otherwise, the PNG image itself is returned.
=cut
-sub print_text {
- my( $self, $today, $template ) = @_;
-
- my %params = ( 'format' => 'template' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
-
- $self->print_generic( %params );
+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);
+
+ if($dir) {
+ my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
+ DIR => $dir,
+ SUFFIX => '.png',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ print $bh $gd->png or die "cannot write barcode to file: $!\n";
+ my $png_file = $bh->filename;
+ close $bh;
+ return $png_file;
+ }
+ return $gd->png;
}
-=item print_latex [ TIME [ , TEMPLATE ] ]
-
-Internal method - returns a filename of a filled-in LaTeX template for this
-invoice (Note: add ".tex" to get the actual filename), and a filename of
-an associated logo (with the .eps extension included).
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
+=item invnum_date_pretty
-TIME an optional value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)"
=cut
-sub print_latex {
-
- my( $self, $today, $template ) = @_;
-
- my %params = ( 'format' => 'latex' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
-
- $template ||= $self->_agent_template;
-
- my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
- my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
- DIR => $dir,
- SUFFIX => '.eps',
- UNLINK => 0,
- ) or die "can't open temp file: $!\n";
-
- if ($template && $conf->exists("logo_${template}.eps")) {
- print $lh $conf->config_binary("logo_${template}.eps")
- or die "can't write temp file: $!\n";
- }else{
- print $lh $conf->config_binary('logo.eps')
- or die "can't write temp file: $!\n";
- }
- close $lh;
- $params{'logo_file'} = $lh->filename;
-
- my @filled_in = $self->print_generic( %params );
-
- my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
- DIR => $dir,
- SUFFIX => '.tex',
- UNLINK => 0,
- ) or die "can't open temp file: $!\n";
- print $fh join('', @filled_in );
- close $fh;
-
- $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
- return ($1, $params{'logo_file'});
-
+sub invnum_date_pretty {
+ my $self = shift;
+ $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
}
-=item print_generic OPTIONS_HASH
-
-Internal method - returns a filled-in template for this invoice as a scalar.
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Non optional options include
- format - latex, html, template
-
-Optional options include
-
-template - a value used as a suffix for a configuration template
-
-time - a value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-cid -
-
-unsquelch_cdr - overrides any per customer cdr squelching when true
-
-=cut
-
-sub print_generic {
-
- my( $self, %params ) = @_;
- my $today = $params{today} ? $params{today} : time;
- warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
- if $DEBUG;
-
- my $format = $params{format};
- die "Unknown format: $format"
- unless $format =~ /^(latex|html|template)$/;
-
- my $cust_main = $self->cust_main;
- $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
- unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
-
-
- my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
- 'html' => [ '<%=', '%>' ],
- 'template' => [ '{', '}' ],
- );
-
- #create the template
- my $template = $params{template} ? $params{template} : $self->_agent_template;
- my $templatefile = "invoice_$format";
- $templatefile .= "_$template"
- if length($template);
- my @invoice_template = map "$_\n", $conf->config($templatefile)
- or die "cannot load config data $templatefile";
-
- my $old_latex = '';
- if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
- #change this to a die when the old code is removed
- warn "old-style invoice template $templatefile; ".
- "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
- $old_latex = 'true';
- @invoice_template = _translate_old_latex_format(@invoice_template);
- }
-
- my $text_template = new Text::Template(
- TYPE => 'ARRAY',
- SOURCE => \@invoice_template,
- DELIMITERS => $delimiters{$format},
- );
-
- $text_template->compile()
- or die "Can't compile $templatefile: $Text::Template::ERROR\n";
-
-
- # additional substitution could possibly cause breakage in existing templates
- my %convert_maps = (
- 'latex' => {
- 'notes' => sub { map "$_", @_ },
- 'footer' => sub { map "$_", @_ },
- 'smallfooter' => sub { map "$_", @_ },
- 'returnaddress' => sub { map "$_", @_ },
- 'coupon' => sub { map "$_", @_ },
- },
- 'html' => {
- 'notes' =>
- sub {
- map {
- s/%%(.*)$/<!-- $1 -->/g;
- s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
- s/\\begin\{enumerate\}/<ol>/g;
- s/\\item / <li>/g;
- s/\\end\{enumerate\}/<\/ol>/g;
- s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
- s/\\\\\*/<br>/g;
- s/\\dollar ?/\$/g;
- s/\\#/#/g;
- s/~/ /g;
- $_;
- } @_
- },
- 'footer' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
- 'smallfooter' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
- 'returnaddress' =>
- sub {
- map {
- s/~/ /g;
- s/\\\\\*?\s*$/<BR>/;
- s/\\hyphenation\{[\w\s\-]+}//;
- $_;
- } @_
- },
- 'coupon' => sub { "" },
- },
- 'template' => {
- 'notes' =>
- sub {
- map {
- s/%%.*$//g;
- s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
- s/\\begin\{enumerate\}//g;
- s/\\item / * /g;
- s/\\end\{enumerate\}//g;
- s/\\textbf\{(.*)\}/$1/g;
- s/\\\\\*/ /;
- s/\\dollar ?/\$/g;
- $_;
- } @_
- },
- 'footer' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
- 'smallfooter' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
- 'returnaddress' =>
- sub {
- map {
- s/~/ /g;
- s/\\\\\*?\s*$/\n/; # dubious
- s/\\hyphenation\{[\w\s\-]+}//;
- $_;
- } @_
- },
- 'coupon' => sub { "" },
- },
- );
-
-
- # hashes for differing output formats
- my %nbsps = ( 'latex' => '~',
- 'html' => '', # '&nbps;' would be nice
- 'template' => '', # not used
- );
- my $nbsp = $nbsps{$format};
-
- my %escape_functions = ( 'latex' => \&_latex_escape,
- 'html' => \&encode_entities,
- 'template' => sub { shift },
- );
- my $escape_function = $escape_functions{$format};
-
- my %date_formats = ( 'latex' => '%b %o, %Y',
- 'html' => '%b %o, %Y',
- 'template' => '%s',
- );
- my $date_format = $date_formats{$format};
-
- my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
- },
- 'html' => sub { return '<b>'. shift(). '</b>'
- },
- 'template' => sub { shift },
- );
- my $embolden_function = $embolden_functions{$format};
-
-
- # generate template variables
- my $returnaddress;
- if (
- defined( $conf->config_orbase( "invoice_${format}returnaddress",
- $template
- )
- )
- && length( $conf->config_orbase( "invoice_${format}returnaddress",
- $template
- )
- )
- ) {
-
- $returnaddress = join("\n",
- $conf->config_orbase("invoice_${format}returnaddress", $template)
- );
-
- } elsif ( grep /\S/,
- $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
-
- my $convert_map = $convert_maps{$format}{'returnaddress'};
- $returnaddress =
- join( "\n",
- &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
- $template
- )
- )
- );
- } elsif ( grep /\S/, $conf->config('company_address') ) {
-
- my $convert_map = $convert_maps{$format}{'returnaddress'};
- $returnaddress = join( "\n", &$convert_map(
- map { s/( {2,})/'~' x length($1)/eg;
- s/$/\\\\\*/;
- $_
- }
- ( $conf->config('company_name'),
- $conf->config('company_address'),
- )
- )
- );
+#sub _items_extra_usage_sections {
+# my $self = shift;
+# my $escape = shift;
+#
+# my %sections = ();
+#
+# my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
+# foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+# {
+# next unless $cust_bill_pkg->pkgnum > 0;
+#
+# foreach my $section ( keys %usage_class ) {
+#
+# my $usage = $cust_bill_pkg->usage($section);
+#
+# next unless $usage && $usage > 0;
+#
+# $sections{$section} ||= 0;
+# $sections{$section} += $usage;
+#
+# }
+#
+# }
+#
+# map { { 'description' => &{$escape}($_),
+# 'subtotal' => $sections{$_},
+# 'summarized' => '',
+# 'tax_section' => '',
+# }
+# }
+# sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
- } else {
+sub _items_extra_usage_sections {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ foreach my $classnum ( keys %usage_class ) {
+ my $section = $usage_class{$classnum}->classname;
+ $classnums{$section} = $classnum;
+
+ 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 $description = $desc;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
+
+ $lines{$section}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ product_code => 'N/A',
+ ext_description => [],
+ };
- my $warning = "Couldn't find a return address; ".
- "do you need to set the company_address configuration value?";
- warn "$warning\n";
- $returnaddress = $nbsp;
- #$returnaddress = $warning;
+ $lines{$section}{$desc}{amount} += $amount;
+ $lines{$section}{$desc}{calls}++;
+ $lines{$section}{$desc}{duration} += $detail->duration;
+ }
+ }
}
- my %invoice_data = (
- 'company_name' => scalar( $conf->config('company_name') ),
- 'company_address' => join("\n", $conf->config('company_address') ). "\n",
- 'custnum' => $cust_main->display_custnum,
- 'invnum' => $self->invnum,
- 'date' => time2str($date_format, $self->_date),
- 'today' => time2str('%b %o, %Y', $today),
- 'agent' => &$escape_function($cust_main->agent->agent),
- 'agent_custid' => &$escape_function($cust_main->agent_custid),
- 'payname' => &$escape_function($cust_main->payname),
- 'company' => &$escape_function($cust_main->company),
- 'address1' => &$escape_function($cust_main->address1),
- 'address2' => &$escape_function($cust_main->address2),
- 'city' => &$escape_function($cust_main->city),
- 'state' => &$escape_function($cust_main->state),
- 'zip' => &$escape_function($cust_main->zip),
- 'returnaddress' => $returnaddress,
- #'quantity' => 1,
- 'terms' => $self->terms,
- 'template' => $params{'template'},
- #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
- # better hang on to conf_dir for a while
- 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
- 'page' => 1,
- 'total_pages' => 1,
- 'current_charges' => sprintf("%.2f", $self->charged),
- 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
- 'ship_enable' => $conf->exists('invoice-ship_address'),
- 'unitprices' => $conf->exists('invoice-unitprice'),
- );
-
- my $countrydefault = $conf->config('countrydefault') || 'US';
- my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
- foreach ( qw( contact company address1 address2 city state zip country fax) ){
- my $method = $prefix.$_;
- $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+ my %sectionmap = ();
+ foreach (keys %sections) {
+ my $usage_class = $usage_class{$classnums{$_}};
+ $sectionmap{$_} = { 'description' => &{$escape}($_),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'sort_weight' => $usage_class->weight,
+ ( $usage_class->format
+ ? ( map { $_ => $usage_class->$_($format) }
+ qw( description_generator header_generator total_generator total_line_generator )
+ )
+ : ()
+ ),
+ };
}
- $invoice_data{'ship_country'} = ''
- if ( $invoice_data{'ship_country'} eq $countrydefault );
-
- $invoice_data{'cid'} = $params{'cid'}
- if $params{'cid'};
- if ( $cust_main->country eq $countrydefault ) {
- $invoice_data{'country'} = '';
- } else {
- $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
+ my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
}
- my @address = ();
- $invoice_data{'address'} = \@address;
- push @address,
- $cust_main->payname.
- ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
- ? " (P.O. #". $cust_main->payinfo. ")"
- : ''
- )
- ;
- push @address, $cust_main->company
- if $cust_main->company;
- push @address, $cust_main->address1;
- push @address, $cust_main->address2
- if $cust_main->address2;
- push @address,
- $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
- push @address, $invoice_data{'country'}
- if $invoice_data{'country'};
- push @address, ''
- while (scalar(@address) < 5);
-
- $invoice_data{'logo_file'} = $params{'logo_file'}
- if $params{'logo_file'};
-
- my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
- #my $balance_due = $self->owed + $pr_total - $cr_total;
- my $balance_due = $self->owed + $pr_total;
- $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
- $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+ return(\@sections, \@lines);
+
+}
+
+sub _did_summary {
+ my $self = shift;
+ my $end = $self->_date;
+
+ # start at date of previous invoice + 1 second or 0 if no previous invoice
+ my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
+ $start = 0 if !$start;
+ $start++;
+
+ my $cust_main = $self->cust_main;
+ my @pkgs = $cust_main->all_pkgs;
+ my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+ = (0,0,0,0,0);
+ my @seen = ();
+ foreach my $pkg ( @pkgs ) {
+ my @h_cust_svc = $pkg->h_cust_svc($end);
+ foreach my $h_cust_svc ( @h_cust_svc ) {
+ next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+ next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+
+ my $inserted = $h_cust_svc->date_inserted;
+ my $deleted = $h_cust_svc->date_deleted;
+ 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 eq ''
+ || $phone_inserted->lnp_status eq 'native')) {
+ $num_activated++;
+ }
+ else { # this one not so clean, should probably move to (h_)svc_phone
+ my $phone_portedin = qsearchs( 'h_svc_phone',
+ { '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 ne 'portingout')) {
+ $num_deactivated++;
+ }
+ elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+ && $phone_deleted->lnp_status
+ && $phone_deleted->lnp_status eq 'portingout') {
+ $num_portedout++;
+ }
+
+ # increment usage minutes
+ if ( $phone_inserted ) {
+ my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
+ $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
+ }
+ else {
+ warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
+ }
- #do variable substitution in notes, footer, smallfooter
- foreach my $include (qw( notes footer smallfooter coupon )) {
+ # don't look at this service again
+ push @seen, $h_cust_svc->svcnum;
+ }
+ }
- my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
- my @inc_src;
+ $minutes = sprintf("%d", $minutes);
+ ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
+ . "$num_deactivated Ported-Out: $num_portedout ",
+ "Total Minutes: $minutes");
+}
+
+sub _items_accountcode_cdr {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my $section = { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => '',
+ 'phonenum' => '',
+ 'description' => 'Usage by Account Code',
+ 'post_total' => '',
+ 'summarized' => '',
+ 'header' => '',
+ };
+ my @lines;
+ my %accountcodes = ();
- if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
- @inc_src = $conf->config($inc_file);
+ my @header = $cust_bill_pkg->details_header;
+ next unless scalar(@header);
+ $section->{'header'} = join(',',@header);
- } else {
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
- $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+ $section->{'header'} = $detail->formatted('format' => $format)
+ if($detail->detail eq $section->{'header'});
+
+ my $accountcode = $detail->accountcode;
+ next unless $accountcode;
+
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $accountcodes{$accountcode} ||= {
+ description => $accountcode,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ quantity => '',
+ product_code => 'N/A',
+ section => $section,
+ ext_description => [ $section->{'header'} ],
+ detail_temp => [],
+ };
+
+ $section->{'amount'} += $amount;
+ $accountcodes{$accountcode}{'amount'} += $amount;
+ $accountcodes{$accountcode}{calls}++;
+ $accountcodes{$accountcode}{duration} += $detail->duration;
+ push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
+ }
+ }
- my $convert_map = $convert_maps{$format}{$include};
+ foreach my $l ( values %accountcodes ) {
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
+ foreach my $sorted_detail ( @sorted_detail ) {
+ push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
+ }
+ delete $l->{detail_temp};
+ push @lines, $l;
+ }
- @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
- s/--\@\]/$delimiters{$format}[1]/g;
- $_;
- }
- &$convert_map( $conf->config($inc_file) );
+ my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
- }
+ return ($section,\@sorted_lines);
+}
- my $inc_tt = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @inc_src ],
- DELIMITERS => $delimiters{$format},
- ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+sub _items_svc_phone_sections {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ my @header = $cust_bill_pkg->details_header;
+ next unless scalar(@header);
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+ my $phonenum = $detail->phonenum;
+ next unless $phonenum;
+
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$phonenum} ||= { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => -1,
+ 'phonenum' => $phonenum,
+ };
+ $sections{$phonenum}{amount} += $amount; #subtotal
+ $sections{$phonenum}{calls}++;
+ $sections{$phonenum}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
+
+ $lines{$phonenum}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
- unless ( $inc_tt->compile() ) {
- my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
- warn $error. "Template:\n". join('', map "$_\n", @inc_src);
- die $error;
- }
+ $lines{$phonenum}{$desc}{amount} += $amount;
+ $lines{$phonenum}{$desc}{calls}++;
+ $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+ my $line = $usage_class{$detail->classnum}->classname;
+ $sections{"$phonenum $line"} ||=
+ { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => $usage_class{$detail->classnum}->weight,
+ 'phonenum' => $phonenum,
+ 'header' => [ @header ],
+ };
+ $sections{"$phonenum $line"}{amount} += $amount; #subtotal
+ $sections{"$phonenum $line"}{calls}++;
+ $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+ $lines{"$phonenum $line"}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
- $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+ $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+ $lines{"$phonenum $line"}{$desc}{calls}++;
+ $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+ push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+ $detail->formatted('format' => $format);
- $invoice_data{$include} =~ s/\n+$//
- if ($format eq 'latex');
+ }
}
- $invoice_data{'po_line'} =
- ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
- ? &$escape_function("Purchase Order #". $cust_main->payinfo)
- : $nbsp;
-
- my %money_chars = ( 'latex' => '',
- 'html' => $conf->config('money_char') || '$',
- 'template' => '',
- );
- my $money_char = $money_chars{$format};
-
- my %other_money_chars = ( 'latex' => '\dollar ',
- 'html' => $conf->config('money_char') || '$',
- 'template' => '',
- );
- my $other_money_char = $other_money_chars{$format};
-
- my @detail_items = ();
- my @total_items = ();
- my @buf = ();
- my @sections = ();
-
- $invoice_data{'detail_items'} = \@detail_items;
- $invoice_data{'total_items'} = \@total_items;
- $invoice_data{'buf'} = \@buf;
- $invoice_data{'sections'} = \@sections;
-
- my $previous_section = { 'description' => 'Previous Charges',
- 'subtotal' => $other_money_char.
- sprintf('%.2f', $pr_total),
- };
-
- my $taxtotal = 0;
- my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
- 'subtotal' => $taxtotal }; # adjusted below
-
- my $adjusttotal = 0;
- my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
- 'subtotal' => 0 }; # adjusted below
-
- my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
- my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
- my $late_sections = [];
- if ( $multisection ) {
- push @sections, $self->_items_sections( $late_sections );
- }else{
- push @sections, { 'description' => '', 'subtotal' => '' };
+ my %sectionmap = ();
+ my $simple = new FS::usage_class { format => 'simple' }; #bleh
+ foreach ( keys %sections ) {
+ my @header = @{ $sections{$_}{header} || [] };
+ my $usage_simple =
+ new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
+ my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+ my $usage_class = $summary ? $simple : $usage_simple;
+ my $ending = $summary ? ' usage charges' : '';
+ my %gen_opt = ();
+ unless ($summary) {
+ $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+ }
+ $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'phonenum' => $sections{$_}{phonenum},
+ 'sort_weight' => $sections{$_}{sort_weight},
+ 'post_total' => $summary, #inspire pagebreak
+ (
+ ( map { $_ => $usage_class->$_($format, %gen_opt) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ )
+ ),
+ };
}
- foreach my $line_item ( $conf->exists('disable_previous_balance')
- ? ()
- : $self->_items_previous
- )
- {
- my $detail = {
- ext_description => [],
- };
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'quantity'} = 1;
- $detail->{'section'} = $previous_section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = map {
- &$escape_function($_);
- } @{$line_item->{'ext_description'}};
+ my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+ $a->{sort_weight} <=> $b->{sort_weight}
+ }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
}
- $detail->{'amount'} = ( $old_latex ? '' : $money_char).
- $line_item->{'amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
- push @detail_items, $detail;
- push @buf, [ $detail->{'description'},
- $money_char. sprintf("%10.2f", $line_item->{'amount'}),
- ];
}
- if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
- push @buf, ['','-----------'];
- push @buf, [ 'Total Previous Balance',
- $money_char. sprintf("%10.2f", $pr_total) ];
- push @buf, ['',''];
- }
+ if($conf->exists('phone_usage_class_summary')) {
+ # this only works with Latex
+ my @newlines;
+ my @newsections;
+
+ # after this, we'll have only two sections per DID:
+ # Calls Summary and Calls Detail
+ foreach my $section ( @sections ) {
+ if($section->{'post_total'}) {
+ $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+ $section->{'total_line_generator'} = sub { '' };
+ $section->{'total_generator'} = sub { '' };
+ $section->{'header_generator'} = sub { '' };
+ $section->{'description_generator'} = '';
+ push @newsections, $section;
+ my %calls_detail = %$section;
+ $calls_detail{'post_total'} = '';
+ $calls_detail{'sort_weight'} = '';
+ $calls_detail{'description_generator'} = sub { '' };
+ $calls_detail{'header_generator'} = sub {
+ return ' & Date/Time & Called Number & Duration & Price'
+ if $format eq 'latex';
+ '';
+ };
+ $calls_detail{'description'} = 'Calls Detail: '
+ . $section->{'phonenum'};
+ push @newsections, \%calls_detail;
+ }
+ }
- foreach my $section (@sections, @$late_sections) {
+ # after this, each usage class is collapsed/summarized into a single
+ # line under the Calls Summary section
+ foreach my $newsection ( @newsections ) {
+ if($newsection->{'post_total'}) { # this means Calls Summary
+ foreach my $section ( @sections ) {
+ next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+ && !$section->{'post_total'});
+ my $newdesc = $section->{'description'};
+ my $tn = $section->{'phonenum'};
+ $newdesc =~ s/$tn//g;
+ my $line = { ext_description => [],
+ pkgnum => '',
+ ref => '',
+ quantity => '',
+ calls => $section->{'calls'},
+ section => $newsection,
+ duration => $section->{'duration'},
+ description => $newdesc,
+ amount => sprintf("%.2f",$section->{'amount'}),
+ product_code => 'N/A',
+ };
+ push @newlines, $line;
+ }
+ }
+ }
- $section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $section->{'subtotal'})
- if $multisection;
+ # after this, Calls Details is populated with all CDRs
+ foreach my $newsection ( @newsections ) {
+ if(!$newsection->{'post_total'}) { # this means Calls Details
+ foreach my $line ( @lines ) {
+ next unless (scalar(@{$line->{'ext_description'}}) &&
+ $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
+ );
+ my @extdesc = @{$line->{'ext_description'}};
+ my @newextdesc;
+ foreach my $extdesc ( @extdesc ) {
+ $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
+ push @newextdesc, $extdesc;
+ }
+ $line->{'ext_description'} = \@newextdesc;
+ $line->{'section'} = $newsection;
+ push @newlines, $line;
+ }
+ }
+ }
- if ( $section->{'description'} ) {
- push @buf, ( [ &$escape_function($section->{'description'}), '' ],
- [ '', '' ],
- );
- }
+ return(\@newsections, \@newlines);
+ }
- my %options = ();
- $options{'section'} = $section if $multisection;
- $options{'format'} = $format;
- $options{'escape_function'} = $escape_function;
- $options{'format_function'} = sub { () } unless $unsquelched;
- $options{'unsquelched'} = $unsquelched;
+ return(\@sections, \@lines);
- foreach my $line_item ( $self->_items_pkg(%options) ) {
- my $detail = {
- ext_description => [],
- };
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'quantity'} = $line_item->{'quantity'};
- $detail->{'section'} = $section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
- }
- $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
- $line_item->{'amount'};
- $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
- $line_item->{'unit_amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
- push @detail_items, $detail;
- push @buf, ( [ $detail->{'description'},
- $money_char. sprintf("%10.2f", $line_item->{'amount'}),
- ],
- map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
- );
- }
-
- if ( $section->{'description'} ) {
- push @buf, ( ['','-----------'],
- [ $section->{'description'}. ' sub-total',
- $money_char. sprintf("%10.2f", $section->{'subtotal'})
- ],
- [ '', '' ],
- [ '', '' ],
- );
- }
-
- }
-
- if ( $multisection && !$conf->exists('disable_previous_balance') ) {
- unshift @sections, $previous_section if $pr_total;
- }
-
- foreach my $tax ( $self->_items_tax ) {
- my $total = {};
- $total->{'total_item'} = &$escape_function($tax->{'description'});
- $taxtotal += $tax->{'amount'};
- $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($tax->{'description'}),
- amount => $money. $tax->{'amount'},
- product_code => '',
- section => $tax_section,
- };
- }else{
- push @total_items, $total;
- }
- push @buf,[ $total->{'total_item'},
- $money_char. sprintf("%10.2f", $total->{'total_amount'}),
- ];
-
- }
-
- if ( $taxtotal ) {
- my $total = {};
- $total->{'total_item'} = 'Sub-total';
- $total->{'total_amount'} =
- $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-
- if ( $multisection ) {
- $tax_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $taxtotal);
- $tax_section->{'pretotal'} = 'New charges sub-total '.
- $total->{'total_amount'};
- push @sections, $tax_section if $taxtotal;
- }else{
- unshift @total_items, $total;
- }
- }
- $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
-
- push @buf,['','-----------'];
- push @buf,[( $conf->exists('disable_previous_balance')
- ? 'Total Charges'
- : 'Total New Charges'
- ),
- $money_char. sprintf("%10.2f",$self->charged) ];
- push @buf,['',''];
-
- {
- my $total = {};
- $total->{'total_item'} = &$embolden_function('Total');
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char.
- sprintf( '%.2f',
- $self->charged + ( $conf->exists('disable_previous_balance')
- ? 0
- : $pr_total
- )
- )
- );
- if ( $multisection ) {
- $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
- sprintf('%.2f', $self->charged );
- }else{
- push @total_items, $total;
- }
- push @buf,['','-----------'];
- push @buf,['Total Charges',
- $money_char.
- sprintf( '%10.2f', $self->charged +
- ( $conf->exists('disable_previous_balance')
- ? 0
- : $pr_total
- )
- )
- ];
- push @buf,['',''];
- }
-
- unless ( $conf->exists('disable_previous_balance') ) {
- #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-
- # credits
- my $credittotal = 0;
- foreach my $credit ( $self->_items_credits ) {
- my $total;
- $total->{'total_item'} = &$escape_function($credit->{'description'});
- $credittotal += $credit->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
- $adjusttotal += $credit->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($credit->{'description'}),
- amount => $money. $credit->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- }else{
- push @total_items, $total;
- }
- }
- $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
- # credits (again)
- foreach ( $self->cust_credited ) {
-
- #something more elaborate if $_->amount ne $_->cust_credit->credited ?
-
- my $reason = substr($_->cust_credit->reason,0,32);
- $reason .= '...' if length($reason) < length($_->cust_credit->reason);
- $reason = " ($reason) " if $reason;
- push @buf,[
- "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
- $money_char. sprintf("%10.2f",$_->amount)
- ];
- }
-
- # payments
- my $paymenttotal = 0;
- foreach my $payment ( $self->_items_payments ) {
- my $total = {};
- $total->{'total_item'} = &$escape_function($payment->{'description'});
- $paymenttotal += $payment->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
- $adjusttotal += $payment->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($payment->{'description'}),
- amount => $money. $payment->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- }else{
- push @total_items, $total;
- }
- push @buf, [ $payment->{'description'},
- $money_char. sprintf("%10.2f", $payment->{'amount'}),
- ];
- }
- $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-
- if ( $multisection ) {
- $adjust_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $adjusttotal);
- push @sections, $adjust_section;
- }
-
- {
- my $total;
- $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
- );
- if ( $multisection ) {
- $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
- $total->{'total_amount'};
- }else{
- push @total_items, $total;
- }
- push @buf,['','-----------'];
- push @buf,[$self->balance_due_msg, $money_char.
- sprintf("%10.2f", $balance_due ) ];
- }
- }
-
- if ( $multisection ) {
- push @sections, @$late_sections
- if $unsquelched;
- }
-
- $invoice_lines = 0;
- my $wasfunc = 0;
- foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
- /invoice_lines\((\d*)\)/;
- $invoice_lines += $1 || scalar(@buf);
- $wasfunc=1;
- }
- die "no invoice_lines() functions in template?"
- if ( $format eq 'template' && !$wasfunc );
-
- if ($format eq 'template') {
-
- if ( $invoice_lines ) {
- $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
- $invoice_data{'total_pages'}++
- if scalar(@buf) % $invoice_lines;
- }
-
- #setup subroutine for the template
- sub FS::cust_bill::_template::invoice_lines {
- my $lines = shift || scalar(@FS::cust_bill::_template::buf);
- map {
- scalar(@FS::cust_bill::_template::buf)
- ? shift @FS::cust_bill::_template::buf
- : [ '', '' ];
- }
- ( 1 .. $lines );
- }
-
- my $lines;
- my @collect;
- while (@buf) {
- push @collect, split("\n",
- $text_template->fill_in( HASH => \%invoice_data,
- PACKAGE => 'FS::cust_bill::_template'
- )
- );
- $FS::cust_bill::_template::page++;
- }
- map "$_\n", @collect;
- }else{
- warn "filling in template for invoice ". $self->invnum. "\n"
- if $DEBUG;
- warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
- if $DEBUG > 1;
-
- $text_template->fill_in(HASH => \%invoice_data);
- }
-}
-
-=item print_ps [ TIME [ , TEMPLATE ] ]
-
-Returns an postscript invoice, as a scalar.
-
-TIME an optional value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=cut
-
-sub print_ps {
- my $self = shift;
-
- my ($file, $lfile) = $self->print_latex(@_);
- my $ps = generate_ps($file);
- unlink($lfile);
-
- $ps;
-}
-
-=item print_pdf [ TIME [ , TEMPLATE ] ]
-
-Returns an PDF invoice, as a scalar.
-
-TIME an optional value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=cut
-
-sub print_pdf {
- my $self = shift;
-
- my ($file, $lfile) = $self->print_latex(@_);
- my $pdf = generate_pdf($file);
- unlink($lfile);
-
- $pdf;
-}
-
-=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
-
-Returns an HTML invoice, as a scalar.
-
-TIME an optional value used to control the printing of overdue messages. The
-default is now. It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
-when emailing the invoice as part of a multipart/related MIME email.
-
-=cut
-
-sub print_html {
- my $self = shift;
- my %params;
- if ( ref $_[0] ) {
- %params = %{ shift() };
- }else{
- $params{'time'} = shift;
- $params{'template'} = shift;
- $params{'cid'} = shift;
- }
-
- $params{'format'} = 'html';
-
- $self->print_generic( %params );
-}
-
-# quick subroutine for print_latex
-#
-# There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves:
-# # $ % & ~ _ ^ \ { }
-#
-# TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
-
-sub _latex_escape {
- my $value = shift;
- $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
- $value =~ s/([<>])/\$$1\$/g;
- $value;
-}
-
-#utility methods for print_*
-
-sub _translate_old_latex_format {
- warn "_translate_old_latex_format called\n"
- if $DEBUG;
-
- my @template = ();
- while ( @_ ) {
- my $line = shift;
-
- if ( $line =~ /^%%Detail\s*$/ ) {
-
- push @template, q![@--!,
- q! foreach my $_tr_line (@detail_items) {!,
- q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
- q! $_tr_line->{'description'} .= !,
- q! "\\tabularnewline\n~~".!,
- q! join( "\\tabularnewline\n~~",!,
- q! @{$_tr_line->{'ext_description'}}!,
- q! );!,
- q! }!;
-
- while ( ( my $line_item_line = shift )
- !~ /^%%EndDetail\s*$/ ) {
- $line_item_line =~ s/'/\\'/g; # nice LTS
- $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
- $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
- push @template, " \$OUT .= '$line_item_line';";
- }
-
- push @template, '}',
- '--@]';
-
- } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
-
- push @template, '[@--',
- ' foreach my $_tr_line (@total_items) {';
-
- while ( ( my $total_item_line = shift )
- !~ /^%%EndTotalDetails\s*$/ ) {
- $total_item_line =~ s/'/\\'/g; # nice LTS
- $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
- $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
- push @template, " \$OUT .= '$total_item_line';";
- }
-
- push @template, '}',
- '--@]';
-
- } else {
- $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
- push @template, $line;
- }
-
- }
-
- if ($DEBUG) {
- warn "$_\n" foreach @template;
- }
-
- (@template);
-}
-
-sub terms {
- my $self = shift;
-
- #check for an invoice- specific override (eventually)
-
- #check for a customer- specific override
- return $self->cust_main->invoice_terms
- if $self->cust_main->invoice_terms;
-
- #use configured default or default default
- $conf->config('invoice_default_terms') || 'Payable upon receipt';
-}
-
-sub due_date {
- my $self = shift;
- my $duedate = '';
- if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = $self->_date() + ( $1 * 86400 );
- }
- $duedate;
-}
-
-sub due_date2str {
- my $self = shift;
- $self->due_date ? time2str(shift, $self->due_date) : '';
-}
-
-sub balance_due_msg {
- my $self = shift;
- my $msg = 'Balance Due';
- return $msg unless $self->terms;
- if ( $self->due_date ) {
- $msg .= ' - Please pay by '. $self->due_date2str('%x');
- } elsif ( $self->terms ) {
- $msg .= ' - '. $self->terms;
- }
- $msg;
-}
-
-sub balance_due_date {
- my $self = shift;
- my $duedate = '';
- if ( $conf->exists('invoice_default_terms')
- && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
- }
- $duedate;
-}
-
-=item invnum_date_pretty
-
-Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
-
-=cut
-
-sub invnum_date_pretty {
- my $self = shift;
- 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
-}
-
-sub _items_sections {
- my $self = shift;
- my $late = shift;
-
- my %s = ();
- my %l = ();
-
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
- {
-
- if ( $cust_bill_pkg->pkgnum > 0 ) {
- my $usage = $cust_bill_pkg->usage;
-
- foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
- my $desc = $display->section;
- my $type = $display->type;
-
- if ( $display->post_total ) {
- if (! $type || $type eq 'S') {
- $l{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
- }
-
- if (! $type) {
- $l{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
- }
-
- if ($type && $type eq 'R') {
- $l{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
- }
-
- if ($type && $type eq 'U') {
- $l{$desc} += $usage;
- }
-
- } else {
- if (! $type || $type eq 'S') {
- $s{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
- }
-
- if (! $type) {
- $s{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
- }
-
- if ($type && $type eq 'R') {
- $s{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
- }
-
- if ($type && $type eq 'U') {
- $s{$desc} += $usage;
- }
-
- }
-
- }
-
- }
-
- }
-
- push @$late, map { { 'description' => $_,
- 'subtotal' => $l{$_},
- 'post_total' => 1,
- } } sort keys %l;
-
- map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
-
-}
-
-sub _items {
- my $self = shift;
-
- #my @display = scalar(@_)
- # ? @_
- # : qw( _items_previous _items_pkg );
- # #: qw( _items_pkg );
- # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
- my @display = qw( _items_previous _items_pkg );
-
- my @b = ();
- foreach my $display ( @display ) {
- push @b, $self->$display(@_);
- }
- @b;
-}
+}
sub _items_previous {
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' => 'Previous Balance, Invoice #'. $_->invnum.
- ' ('. time2str('%x',$_->_date). ')',
+ 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
#'pkgpart' => 'N/A',
'pkgnum' => 'N/A',
'amount' => sprintf("%.2f", $_->owed),
#};
}
-sub _items_pkg {
- my $self = shift;
- my %options = @_;
- my $section = $options{'section'};
- my $desc = $section->{'description'};
- my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
-}
-
-sub _taxsort {
- return 0 unless $a cmp $b;
- return -1 if $b eq 'Tax';
- return 1 if $a eq 'Tax';
- return -1 if $b eq 'Other surcharges';
- return 1 if $a eq 'Other surcharges';
- $a cmp $b;
-}
-
-sub _items_tax {
- my $self = shift;
- my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-}
-
-sub _items_cust_bill_pkg {
- my $self = shift;
- my $cust_bill_pkg = shift;
- my %opt = @_;
-
- my $format = $opt{format} || '';
- my $escape_function = $opt{escape_function} || sub { shift };
- my $format_function = $opt{format_function} || '';
- my $unsquelched = $opt{unsquelched} || '';
- my $section = $opt{section}->{description} if $opt{section};
-
- my @b = ();
- foreach my $cust_bill_pkg ( @$cust_bill_pkg )
- {
- foreach my $display ( grep { defined($section)
- ? $_->section eq $section
- : 1
- }
- $cust_bill_pkg->cust_bill_pkg_display
- )
- {
-
- my $type = $display->type;
-
- my $cust_pkg = $cust_bill_pkg->cust_pkg;
-
- my $desc = $cust_bill_pkg->desc;
-
- my %details_opt = ( 'format' => $format,
- 'escape_function' => $escape_function,
- 'format_function' => $format_function,
- );
-
- if ( $cust_bill_pkg->pkgnum > 0 ) {
-
- if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
-
- my $description = $desc;
- $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-
- my @d = map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date);
- push @d, $cust_bill_pkg->details(%details_opt)
- if $cust_bill_pkg->recur == 0;
-
- push @b, {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => sprintf("%.2f", $cust_bill_pkg->setup),
- unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- };
-
- }
-
- if ( $cust_bill_pkg->recur != 0 &&
- ( !$type || $type eq 'R' || $type eq 'U' )
- )
- {
-
- my $is_summary = $display->summary;
- my $description = $is_summary ? "Usage charges" : $desc;
-
- unless ( $conf->exists('disable_line_item_date_ranges') ) {
- $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
- " - ". time2str("%x", $cust_bill_pkg->edate). ")";
- }
-
- #at least until cust_bill_pkg has "past" ranges in addition to
- #the "future" sdate/edate ones... see #3032
- my @d = ();
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date)
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate),
- ;
-
- @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
- push @d, $cust_bill_pkg->details(%details_opt)
- unless ($is_summary || $type && $type eq 'R');
-
- my $amount = 0;
- if (!$type) {
- $amount = $cust_bill_pkg->recur;
- }elsif($type eq 'R') {
- $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
- }elsif($type eq 'U') {
- $amount = $cust_bill_pkg->usage;
- }
-
- push @b, {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => sprintf("%.2f", $amount),
- unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- };
-
- }
-
- } else { #pkgnum tax or one-shot line item (??)
-
- if ( $cust_bill_pkg->setup != 0 ) {
- push @b, {
- 'description' => $desc,
- 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
- };
- }
- if ( $cust_bill_pkg->recur != 0 ) {
- push @b, {
- 'description' => "$desc (".
- time2str("%x", $cust_bill_pkg->sdate). ' - '.
- time2str("%x", $cust_bill_pkg->edate). ')',
- 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
- };
- }
-
- }
-
- }
-
- }
-
- @b;
-
-}
-
sub _items_credits {
- my $self = shift;
+ my( $self, %opt ) = @_;
+ my $trim_len = $opt{'trim_len'} || 60;
my @b;
#credits
#something more elaborate if $_->amount ne $_->cust_credit->credited ?
- my $reason = $_->cust_credit->reason;
- #my $reason = substr($_->cust_credit->reason,0,32);
- #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
+ my $reason = substr($_->cust_credit->reason, 0, $trim_len);
+ $reason .= '...' if length($reason) < length($_->cust_credit->reason);
$reason = " ($reason) " if $reason;
+
push @b, {
#'description' => 'Credit ref\#'. $_->crednum.
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
- 'description' => 'Credit applied '.
- time2str("%x",$_->cust_credit->_date). $reason,
+ 'description' => $self->mt('Credit applied').' '.
+ time2str($date_format,$_->cust_credit->_date). $reason,
'amount' => sprintf("%.2f",$_->amount),
};
}
- #foreach ( @cr_cust_credit ) {
- # push @buf,[
- # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
- # $money_char. sprintf("%10.2f",$_->credited)
- # ];
- #}
@b;
#something more elaborate if $_->amount ne ->cust_pay->paid ?
+ my $desc = $self->mt('Payment received').' '.
+ time2str($date_format,$_->cust_pay->_date );
+ $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty)
+ if ( $self->conf->exists('invoice_payment_details') );
+
push @b, {
- 'description' => "Payment received ".
- time2str("%x",$_->cust_pay->_date ),
+ 'description' => $desc,
'amount' => sprintf("%.2f", $_->amount )
};
+
}
@b;
}
+=item call_details [ OPTION => VALUE ... ]
+
+Returns an array of CSV strings representing the call details for this invoice
+The only option available is the boolean prepend_billed_number
+
+=cut
+
+sub call_details {
+ my ($self, %opt) = @_;
+
+ my $format_function = sub { shift };
+
+ if ($opt{prepend_billed_number}) {
+ $format_function = sub {
+ my $detail = shift;
+ my $row = shift;
+
+ $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
+
+ };
+ }
+
+ my @details = map { $_->details( 'format_function' => $format_function,
+ 'escape_function' => sub{ return() },
+ )
+ }
+ grep { $_->pkgnum }
+ $self->cust_bill_pkg;
+ my $header = $details[0];
+ ( $header, grep { $_ ne $header } @details );
+}
+
=back
}
sub re_X {
+ # spool_invoice ftp_invoice fax_invoice print_invoice
my($method, $job, %param ) = @_;
if ( $DEBUG ) {
warn "re_X $method for job $job with param:\n".
my $distinct = '';
my $orderby = 'ORDER BY cust_bill._date';
- my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+ my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
=cut
sub owed_sql {
- my $class = shift;
- 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
+ my ($class, $start, $end) = @_;
+ 'charged - '.
+ $class->paid_sql($start, $end). ' - '.
+ $class->credited_sql($start, $end);
}
=item net_sql
=cut
sub net_sql {
- my $class = shift;
- 'charged - '. $class->credited_sql;
+ my ($class, $start, $end) = @_;
+ 'charged - '. $class->credited_sql($start, $end);
}
=item paid_sql
=cut
sub paid_sql {
- #my $class = shift;
+ my ($class, $start, $end) = @_;
+ $start &&= "AND cust_bill_pay._date <= $start";
+ $end &&= "AND cust_bill_pay._date > $end";
+ $start = '' unless defined($start);
+ $end = '' unless defined($end);
"( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
- WHERE cust_bill.invnum = cust_bill_pay.invnum )";
+ WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
}
=item credited_sql
=cut
sub credited_sql {
- #my $class = shift;
+ my ($class, $start, $end) = @_;
+ $start &&= "AND cust_credit_bill._date <= $start";
+ $end &&= "AND cust_credit_bill._date > $end";
+ $start = '' unless defined($start);
+ $end = '' unless defined($end);
"( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
- WHERE cust_bill.invnum = cust_credit_bill.invnum )";
+ WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
}
-=item search_sql HASHREF
+=item due_date_sql
+
+Returns an SQL fragment to retrieve the due date of an invoice.
+Currently only supported on PostgreSQL.
+
+=cut
+
+sub due_date_sql {
+ my $conf = new FS::Conf;
+'COALESCE(
+ SUBSTRING(
+ COALESCE(
+ cust_bill.invoice_terms,
+ cust_main.invoice_terms,
+ \''.($conf->config('invoice_default_terms') || '').'\'
+ ), E\'Net (\\\\d+)\'
+ )::INTEGER, 0
+) * 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 begin
-
-Epoch date (UNIX timestamp) setting a lower bound for _date values
-
-=item end
+=item _date
-Epoch date (UNIX timestamp) setting an upper bound for _date values
+List reference of start date, end date, as UNIX timestamps.
=item invnum_min
=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
=cut
-sub search_sql {
+sub search_sql_where {
my($class, $param) = @_;
if ( $DEBUG ) {
- warn "$me search_sql called with params: \n".
+ warn "$me search_sql_where called with params: \n".
join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
}
my @search = ();
- if ( $param->{'begin'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date >= $1";
+ #agentnum
+ if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_main.agentnum = $1";
+ }
+
+ #refnum
+ if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_main.refnum = $1";
}
- if ( $param->{'end'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date < $1";
+
+ #custnum
+ if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_bill.custnum = $1";
}
+
+ #customer classnum
+ if ( $param->{'cust_classnum'} ) {
+ my $classnums = $param->{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ $classnums = [ grep /^\d+$/, @$classnums ];
+ push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
+ if @$classnums;
+ }
+
+ #_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";
}
- if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.agentnum = $1";
+
+ #charged
+ if ( $param->{charged} ) {
+ my @charged = ref($param->{charged})
+ ? @{ $param->{charged} }
+ : ($param->{charged});
+
+ push @search, map { s/^charged/cust_bill.charged/; $_; }
+ @charged;
}
- push @search, '0 != '. FS::cust_bill->owed_sql
- if $param->{'open'};
+ 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 )';
}
+ #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+)$/ ) {
warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
}
}
-
push @search, $curuser->agentnums_sql;
join(' AND ', @search );