package FS::cust_bill;
+use base qw( FS::cust_bill::Search FS::Template_Mixin
+ FS::cust_main_Mixin FS::Record
+ );
use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
-use vars qw( $invoice_lines @buf ); #yuck
+use vars qw( $DEBUG $me );
+ # but NOT $conf
+use Carp;
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_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_pkg;
use FS::cust_credit_bill;
use FS::pay_batch;
-use FS::cust_pay_batch;
-use FS::cust_bill_event;
use FS::cust_event;
use FS::part_pkg;
use FS::cust_bill_pay;
-use FS::cust_bill_pay_batch;
-use FS::part_bill_event;
use FS::payby;
-
-@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') || '$';
- $date_format = $conf->config('date_format') || '%x';
- $rdate_format = $conf->config('date_format') || '%m/%d/%Y';
-} );
-
=head1 NAME
FS::cust_bill - Object methods for cust_bill records
$tax_amount = $record->tax;
@lines = $cust_bill->print_text;
- @lines = $cust_bill->print_text $time;
+ @lines = $cust_bill->print_text('time' => $time);
=head1 DESCRIPTION
=back
-Customer info at invoice generation time
+Deprecated fields
=over 4
-=item previous_balance
-
-=item billing_balance
+=item billing_balance - the customer's balance immediately before generating
+this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
+to determine the customer's balance at a specific time.
-=back
-
-Deprecated
-
-=over 4
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one. DEPRECATED.
-=item printed - deprecated
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
=back
=item agent_invid - legacy invoice number
+=item promised_date - customer promised payment date, for collection
+
+=item pending - invoice is still being generated, empty or 'Y'
+
=back
=head1 METHODS
=cut
sub table { 'cust_bill'; }
+sub template_conf { 'invoice_'; }
+
+sub has_sections {
+ my $self = shift;
+ my $agentnum = $self->cust_main->agentnum;
+ my $tc = $self->template_conf;
+
+ $self->conf->exists($tc.'sections', $agentnum) ||
+ $self->conf->exists($tc.'sections_by_location', $agentnum);
+}
+
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+ my $self = shift;
+ $self->conf->config('notice_name') || 'Invoice'
+}
-sub cust_linked { $_[0]->cust_main_custnum; }
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
sub cust_unlinked_msg {
my $self = shift;
"WARNING: can't find cust_main.custnum ". $self->custnum.
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
my $dbh = dbh;
foreach my $table (qw(
- cust_bill_event
- cust_event
cust_credit_bill
cust_bill_pay
- cust_bill_pay
- cust_credit_bill
cust_pay_batch
cust_bill_pay_batch
+ cust_bill_batch
cust_bill_pkg
)) {
+ #cust_event # problematic
foreach my $linked ( $self->$table() ) {
my $error = $linked->delete;
}
-=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->pending eq 'Y'
+ || $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,
|| $self->ut_enum('closed', [ '', 'Y' ])
|| $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
|| $self->ut_numbern('agent_invid') #varchar?
+ || $self->ut_flag('pending')
;
return $error if $error;
sub display_invnum {
my $self = shift;
- if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ if ( $self->agent_invid
+ && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
return $self->agent_invid;
} else {
return $self->invnum;
}
}
+=item previous_bill
+
+Returns the customer's last invoice before this one.
+
+=cut
+
+sub previous_bill {
+ my $self = shift;
+ if ( !$self->get('previous_bill') ) {
+ $self->set('previous_bill', qsearchs({
+ 'table' => 'cust_bill',
+ 'hashref' => { 'custnum' => $self->custnum,
+ '_date' => { op=>'<', value=>$self->_date } },
+ 'order_by' => 'ORDER BY _date DESC LIMIT 1',
+ }) );
+ }
+ $self->get('previous_bill');
+}
+
=item previous
Returns a list consisting of the total previous balance for this customer,
sub previous {
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 } )
- ;
- foreach ( @cust_bill ) { $total += $_->owed; }
- $total, @cust_bill;
+ # simple memoize; we use this a lot
+ if (!$self->get('previous')) {
+ my $total = 0;
+ my @cust_bill = sort { $a->_date <=> $b->_date }
+ grep { $_->owed != 0 }
+ qsearch( 'cust_bill', { 'custnum' => $self->custnum,
+ #'_date' => { op=>'<', value=>$self->_date },
+ 'invnum' => { op=>'<', value=>$self->invnum },
+ } )
+ ;
+ foreach ( @cust_bill ) { $total += $_->owed; }
+ $self->set('previous', [$total, @cust_bill]);
+ }
+ return @{ $self->get('previous') };
+}
+
+=item enable_previous
+
+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
qsearch(
{ 'table' => 'cust_bill_pkg',
'hashref' => { 'invnum' => $self->invnum },
- 'order_by' => 'ORDER BY billpkgnum',
+ 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
+ # the AUTLOADED FK search. or should
+ # that default to ORDER by the pkey?
}
);
}
@open;
}
-=item cust_bill_event
-
-Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub cust_bill_event {
- my $self = shift;
- qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
-}
-
-=item num_cust_bill_event
-
-Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub num_cust_bill_event {
- my $self = shift;
- my $sql =
- "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
- my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
- $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
- $sth->fetchrow_arrayref->[0];
-}
-
=item cust_event
Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
Returns the customer (see L<FS::cust_main>) for this invoice.
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
+
+Returns a list: an empty list on success or a list of errors.
+
=cut
-sub cust_main {
+sub suspend {
my $self = shift;
- qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+ grep { $_->suspend(@_) }
+ grep {! $_->getfield('cancel') }
+ $self->cust_pkg;
+
}
=item cust_suspend_if_balance_over AMOUNT
}
}
-=item cust_credit
+=item cancel
-Depreciated. See the cust_credited method.
-
- #Returns a list consisting of the total previous credited (see
- #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
- #outstanding credits (FS::cust_credit objects).
+Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
=cut
-sub cust_credit {
- use Carp;
- croak "FS::cust_bill->cust_credit depreciated; see ".
- "FS::cust_bill->cust_credit_bill";
- #my $self = shift;
- #my $total = 0;
- #my @cust_credit = sort { $a->_date <=> $b->_date }
- # grep { $_->credited != 0 && $_->_date < $self->_date }
- # qsearch('cust_credit', { 'custnum' => $self->custnum } )
- #;
- #foreach (@cust_credit) { $total += $_->credited; }
- #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated. See the cust_bill_pay method.
+sub cancel {
+ my( $self, %opt ) = @_;
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
+ warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+ join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+ if $DEBUG;
-=cut
+ return ( 'Access denied' )
+ unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
-sub cust_pay {
- use Carp;
- croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
- #my $self = shift;
- #sort { $a->_date <=> $b->_date }
- # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
- #;
-}
+ my @pkgs = $self->cust_pkg;
-sub cust_pay_batch {
- my $self = shift;
- qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
-}
+ if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
+ $opt{nobill} = 1;
+ my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
+ warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+ if $error;
+ }
-sub cust_bill_pay_batch {
- my $self = shift;
- qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+ grep { $_ }
+ map { $_->cancel(%opt) }
+ grep { ! $_->getfield('cancel') }
+ @pkgs;
}
=item cust_bill_pay
shift->cust_credited(@_);
}
-=item cust_bill_pay_pkgnum PKGNUM
+#=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
-with matching pkgnum.
+applied against the matching pkgnum.
=cut
-sub cust_bill_pay_pkgnum {
+sub cust_bill_pay_pkg {
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,
- }
- );
+
+ 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_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_pkgnum PKGNUM
+=item cust_credit_bill_pkg PKGNUM
-Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
-with matching pkgnum.
+Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
+applied against the matching pkgnum.
=cut
-sub cust_credited_pkgnum {
+sub cust_credit_bill_pkg {
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,
- }
- );
+
+ 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",
+ });
+
}
-sub cust_credit_bill_pkgnum {
- shift->cust_credited_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
my $balance = 0;
$balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
- $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
- $balance -= $_->amount for $self->cust_credited_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 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.
sub apply_payments_and_credits {
my( $self, %options ) = @_;
+ my $conf = $self->conf;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
}
-=item generate_email OPTION => VALUE ...
+=item send HASHREF
-Options:
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes. See L<FS::cust_main_invoice>.
-=over 4
+Options can be passed as a hashref. Positional parameters are no longer
+allowed.
-=item from
+I<template>: a suffix for alternate invoices
-sender address, required
+I<agentnum>: obsolete, now does nothing.
-=item tempate
+I<from> overrides the default email invoice From: address.
-alternate template name, optional
+I<amount>: obsolete, does nothing
-=item print_text
+I<notice_name> overrides "Invoice" as the name of the sent document
+(templates from 10/2009 or newer required).
-text attachment arrayref, optional
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
-=item subject
+=cut
-email subject, optional
+sub send {
+ my $self = shift;
+ my $opt = ref($_[0]) ? $_[0] : +{ @_ };
+ my $conf = $self->conf;
-=item notice_name
+ my $cust_main = $self->cust_main;
-notice name instead of "Invoice", optional
+ my @invoicing_list = $cust_main->invoicing_list;
-=back
+ $self->email($opt)
+ if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+ && ! $cust_main->invoice_noemail;
-Returns an argument list to be passed to L<FS::Misc::send_email>.
+ $self->print($opt)
+ if grep { $_ eq 'POST' } @invoicing_list; #postal
-=cut
+ #this has never been used post-$ORIGINAL_ISP afaik
+ $self->fax_invoice($opt)
+ if grep { $_ eq 'FAX' } @invoicing_list; #fax
-use MIME::Entity;
+ '';
-sub generate_email {
+}
+sub email {
my $self = shift;
- my %args = @_;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die ref($self). '->email called with positional parameters';
+ }
- my $me = '[FS::cust_bill::generate_email]';
+ my $conf = $self->conf;
- my %return = (
- 'from' => $args{'from'},
- 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
- );
+ my $from = delete $opt->{from};
- my %opt = (
- 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
- 'template' => $args{'template'},
- 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
- );
+ # this is where we set the From: address
+ $from ||= $self->_agent_invoice_from || #XXX should go away
+ $conf->invoice_from_full( $self->cust_main->agentnum );
- my $cust_main = $self->cust_main;
+ my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
- if (ref($args{'to'}) eq 'ARRAY') {
- $return{'to'} = $args{'to'};
- } else {
- $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
- $cust_main->invoicing_list
- ];
+ if ( ! @invoicing_list ) { #no recipients
+ if ( $conf->exists('cust_bill-no_recipients-error') ) {
+ die 'No recipients for customer #'. $self->custnum;
+ } else {
+ #default: better to notify this person than silence
+ @invoicing_list = ($from);
+ }
}
- if ( $conf->exists('invoice_html') ) {
+ $self->SUPER::email( {
+ 'from' => $from,
+ 'to' => \@invoicing_list,
+ %$opt,
+ });
- warn "$me creating HTML/text multipart message"
- if $DEBUG;
+}
- $return{'nobody'} = 1;
+#this stays here for now because its explicitly used as
+# FS::cust_bill::queueable_email
+sub queueable_email {
+ my %opt = @_;
- my $alternative = build MIME::Entity
- 'Type' => 'multipart/alternative',
- 'Encoding' => '7bit',
- 'Disposition' => 'inline'
- ;
+ my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
+ or die "invalid invoice number: " . $opt{invnum};
- my $data;
- if ( $conf->exists('invoice_email_pdf')
- and scalar($conf->config('invoice_email_pdf_note')) ) {
+ if ( $opt{mode} ) {
+ $self->set('mode', $opt{mode});
+ }
- warn "$me using 'invoice_email_pdf_note' in multipart message"
- if $DEBUG;
- $data = [ map { $_ . "\n" }
- $conf->config('invoice_email_pdf_note')
- ];
+ my %args = map {$_ => $opt{$_}}
+ grep { $opt{$_} }
+ qw( from notice_name no_coupon template );
- } else {
+ my $error = $self->email( \%args );
+ die $error if $error;
- warn "$me not using 'invoice_email_pdf_note' in multipart message"
- if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $data = $args{'print_text'};
- } else {
- $data = [ $self->print_text(\%opt) ];
- }
+}
- }
+sub email_subject {
+ my $self = shift;
+ my $conf = $self->conf;
- $alternative->attach(
- 'Type' => 'text/plain',
- #'Encoding' => 'quoted-printable',
- 'Encoding' => '7bit',
- 'Data' => $data,
- 'Disposition' => 'inline',
- );
+ #my $template = scalar(@_) ? shift : '';
+ #per-template?
- $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);
-
- my $image = build MIME::Entity
- 'Type' => 'image/png',
- 'Encoding' => 'base64',
- 'Data' => $image_data,
- 'Filename' => 'logo.png',
- 'Content-ID' => "<$content_id>",
- ;
+ my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+ || 'Invoice';
- $alternative->attach(
- 'Type' => 'text/html',
- 'Encoding' => 'quoted-printable',
- 'Data' => [ '<html>',
- ' <head>',
- ' <title>',
- ' '. encode_entities($return{'subject'}),
- ' </title>',
- ' </head>',
- ' <body bgcolor="#e8e8e8">',
- $self->print_html({ 'cid'=>$content_id, %opt }),
- ' </body>',
- '</html>',
- ],
- 'Disposition' => 'inline',
- #'Filename' => 'invoice.pdf',
- );
+ 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;
- my @otherparts = ();
- if ( $cust_main->email_csv_cdr ) {
+ eval qq("$subject");
+}
- 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
- # text/html
- # image/png
- # application/pdf
-
- my $related = build MIME::Entity 'Type' => 'multipart/related',
- 'Encoding' => '7bit';
-
- #false laziness w/Misc::send_email
- $related->head->replace('Content-type',
- $related->mime_type.
- '; boundary="'. $related->head->multipart_boundary. '"'.
- '; type=multipart/alternative'
- );
-
- $related->add_part($alternative);
-
- $related->add_part($image);
-
- my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
-
- $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
-
- } else {
-
- #no other attachment:
- # multipart/related
- # multipart/alternative
- # text/plain
- # text/html
- # image/png
-
- $return{'content-type'} = 'multipart/related';
- $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
- $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
- #$return{'disposition'} = 'inline';
-
- }
-
- } else {
-
- if ( $conf->exists('invoice_email_pdf') ) {
- warn "$me creating PDF attachment"
- if $DEBUG;
-
- #mime parts arguments a la MIME::Entity->build().
- $return{'mimeparts'} = [
- { $self->mimebuild_pdf(\%opt) }
- ];
- }
-
- if ( $conf->exists('invoice_email_pdf')
- and scalar($conf->config('invoice_email_pdf_note')) ) {
-
- warn "$me using 'invoice_email_pdf_note'"
- if $DEBUG;
- $return{'body'} = [ map { $_ . "\n" }
- $conf->config('invoice_email_pdf_note')
- ];
-
- } else {
-
- warn "$me not using 'invoice_email_pdf_note'"
- if $DEBUG;
- if ( ref($args{'print_text'}) eq 'ARRAY' ) {
- $return{'body'} = $args{'print_text'};
- } else {
- $return{'body'} = [ $self->print_text(\%opt) ];
- }
-
- }
-
- }
-
- %return;
-
-}
-
-=item mimebuild_pdf
-
-Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
-
-=cut
-
-sub mimebuild_pdf {
- my $self = shift;
- (
- 'Type' => 'application/pdf',
- 'Encoding' => 'base64',
- 'Data' => [ $self->print_pdf(@_) ],
- 'Disposition' => 'attachment',
- 'Filename' => 'invoice-'. $self->invnum. '.pdf',
- );
-}
-
-=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>.
-
-Options can be passed as a hashref (recommended) or as a list of up to
-four values for templatename, agentnum, invoice_from and amount.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<agentnum>, if specified, means that this invoice will only be sent for customers
-of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
-single agent) or an arrayref of agentnums.
-
-I<invoice_from>, if specified, overrides the default email invoice From: address.
-
-I<amount>, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub queueable_send {
- my %opt = @_;
-
- my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
- or die "invalid invoice number: " . $opt{invnum};
-
- my @args = ( $opt{template}, $opt{agentnum} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
-
- my $error = $self->send( @args );
- die $error if $error;
-
-}
-
-sub send {
- my $self = shift;
-
- my( $template, $invoice_from, $notice_name );
- my $agentnums = '';
- my $balance_over = 0;
-
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- if ( $agentnums = $opt->{'agentnum'} ) {
- $agentnums = [ $agentnums ] unless ref($agentnums);
- }
- $invoice_from = $opt->{'invoice_from'};
- $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
- $notice_name = $opt->{'notice_name'};
- } else {
- $template = scalar(@_) ? shift : '';
- if ( scalar(@_) && $_[0] ) {
- $agentnums = ref($_[0]) ? shift : [ shift ];
- }
- $invoice_from = shift if scalar(@_);
- $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
- }
-
- return 'N/A' unless ! $agentnums
- or grep { $_ == $self->cust_main->agentnum } @$agentnums;
-
- return ''
- unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
-
- $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum );
-
- my %opt = (
- 'template' => $template,
- 'invoice_from' => $invoice_from,
- 'notice_name' => ( $notice_name || 'Invoice' ),
- );
-
- my @invoicing_list = $self->cust_main->invoicing_list;
-
- #$self->email_invoice(\%opt)
- $self->email(\%opt)
- if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
-
- #$self->print_invoice(\%opt)
- $self->print(\%opt)
- if grep { $_ eq 'POST' } @invoicing_list; #postal
-
- $self->fax_invoice(\%opt)
- if grep { $_ eq 'FAX' } @invoicing_list; #fax
-
- '';
-
-}
-
-=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
-
-Emails this invoice.
-
-Options can be passed as a hashref (recommended) or as a list of up to
-two values for templatename and invoice_from.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<invoice_from>, if specified, overrides the default email invoice From: address.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub queueable_email {
- my %opt = @_;
-
- my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
- or die "invalid invoice number: " . $opt{invnum};
-
- my @args = ( $opt{template} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
-
- my $error = $self->email( @args );
- die $error if $error;
-
-}
-
-#sub email_invoice {
-sub email {
- my $self = shift;
-
- my( $template, $invoice_from, $notice_name );
- if ( ref($_[0]) ) {
- my $opt = shift;
- $template = $opt->{'template'} || '';
- $invoice_from = $opt->{'invoice_from'};
- $notice_name = $opt->{'notice_name'} || 'Invoice';
- } else {
- $template = scalar(@_) ? shift : '';
- $invoice_from = shift if scalar(@_);
- $notice_name = 'Invoice';
- }
-
- $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;
-
- my $subject = $self->email_subject($template);
-
- my $error = send_email(
- $self->generate_email(
- 'from' => $invoice_from,
- 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
- 'subject' => $subject,
- 'template' => $template,
- 'notice_name' => $notice_name,
- )
- );
- die "can't email invoice: $error\n" if $error;
- #die "$error\n" if $error;
-
-}
-
-sub email_subject {
- my $self = shift;
-
- #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 ]
+=item lpr_data HASHREF
Returns the postscript or plaintext for this invoice as an arrayref.
-Options can be passed as a hashref (recommended) or as a single optional value
-for template.
+Options must be passed as a hashref. Positional parameters are no longer
+allowed.
I<template>, if specified, is the name of a suffix for alternate invoices.
sub lpr_data {
my $self = shift;
- 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 $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ # nobody does this anyway
+ die "FS::cust_bill::lpr_data called with positional parameters";
}
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
- [ $self->$method( \%opt ) ];
+ [ $self->$method( $opt ) ];
}
-=item print HASHREF | [ TEMPLATE ]
+=item print HASHREF
Prints this invoice.
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
I<template>, if specified, is the name of a suffix for alternate invoices.
=cut
-#sub print_invoice {
sub print {
my $self = shift;
- 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';
+ return if $self->hide;
+ my $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::print called with positional parameters";
+ }
+
+ my $lpr = delete $opt->{lpr};
+ if($conf->exists('invoice_print_pdf')) {
+ # Add the invoice to the current batch.
+ $self->batch_invoice($opt);
+ }
+ else {
+ do_print(
+ $self->lpr_data($opt),
+ 'agentnum' => $self->cust_main->agentnum,
+ 'lpr' => $lpr,
+ );
}
-
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- do_print $self->lpr_data(\%opt);
}
-=item fax_invoice HASHREF | [ TEMPLATE ]
+=item fax_invoice HASHREF
Faxes this invoice.
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
I<template>, if specified, is the name of a suffix for alternate invoices.
sub fax_invoice {
my $self = shift;
- 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';
+ return if $self->hide;
+ my $conf = $self->conf;
+ my $opt = shift || {};
+ if ($opt and !ref($opt)) {
+ die "FS::cust_bill::fax_invoice called with positional parameters";
}
die 'FAX invoice destination not (yet?) supported with plain text invoices.'
my $dialstring = $self->cust_main->getfield('fax');
#Check $dialstring?
- my %opt = (
- 'template' => $template,
- 'notice_name' => $notice_name,
- );
-
- my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
+ 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.
+
+HASHREF may contain any options to be passed to C<print_pdf>.
+
+=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(
);
}
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
- my $self = shift;
-
- return ''
- if scalar(
- grep { $_->owed > 0 }
- qsearch('cust_bill', {
- 'custnum' => $self->custnum,
- #'_date' => { op=>'>', value=>$self->_date },
- 'invnum' => { op=>'>', value=>$self->invnum },
- } )
- );
-
- $self->send(@_);
-}
-
=item send_csv OPTION => VALUE, ...
Sends invoice as a CSV data-file to a remote host with the specified protocol.
my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
mkdir $spooldir, 0700 unless -d $spooldir;
+ # don't localize dates here, they're a defined format
my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
my $file = "$spooldir/$tracctnum.csv";
=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';
+ }
+
+ 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';
+ }
- my $file =
- "$spooldir/".
- ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
- ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
- '.csv';
+ $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;
+
+ my $tracctnum = ''; #leaking out from billco-specific sections :/
+ if ( $format eq 'billco' ) {
+
+ my $account_num =
+ $self->conf->config('billco-account_num', $cust_main->agentnum);
+
+ $tracctnum = $account_num eq 'display_custnum'
+ ? $cust_main->display_custnum
+ : $opt{'tracctnum'};
my $taxtotal = 0;
$taxtotal += $_->{'amount'} foreach $self->_items_tax;
- my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+ my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
my( $previous_balance, @unused ) = $self->previous; #previous balance
my $pmt_cr_applied = 0;
$pmt_cr_applied += $_->{'amount'}
- foreach ( $self->_items_payments, $self->_items_credits ) ;
+ foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
$csv->combine(
'', # 1 | N/A-Leave Empty CHAR 2
'', # 2 | N/A-Leave Empty CHAR 15
- $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
+ $tracctnum, # 3 | Transaction Account No CHAR 15
$self->invnum, # 4 | Transaction Invoice No CHAR 15
$cust_main->zip, # 5 | Transaction Zip Code CHAR 5
$cust_main->company, # 6 | Transaction Company Bill To CHAR 30
'0', # 29 | Other Taxes & Fees*** NUM* 9
);
- } 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',
if ( lc($opt{'format'}) eq 'billco' ) {
my $lineseq = 0;
- foreach my $item ( $self->_items_pkg ) {
+ my %items_opt = ( format => 'template',
+ escape_function => sub { shift } );
+ # I don't know what characters billco actually tolerates in spool entries.
+ # Text::CSV will take care of delimiters, though.
+
+ my @items = ( $self->_items_pkg(%items_opt),
+ $self->_items_fee(%items_opt) );
+ foreach my $item (@items) {
+
+ my $description = $item->{'description'};
+ if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
+ $description .= ': ' . $item->{ext_description}[0];
+ }
$csv->combine(
'', # 1 | N/A-Leave Empty CHAR 2
'', # 2 | N/A-Leave Empty CHAR 15
- $opt{'tracctnum'}, # 3 | Account Number CHAR 15
+ $tracctnum, # 3 | Account Number CHAR 15
$self->invnum, # 4 | Invoice Number CHAR 15
$lineseq++, # 5 | Line Sequence (sort order) NUM 6
- $item->{'description'}, # 6 | Transaction Detail CHAR 100
+ $description, # 6 | Transaction Detail CHAR 100
$item->{'amount'}, # 7 | Amount NUM* 9
'', # 8 | Line Format Control** CHAR 2
'', # 9 | Grouping Code CHAR 2
}
+ } elsif ( lc($opt{'format'}) eq 'oneline' ) {
+
+ #do nothing
+
} else {
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
? time2str("%x", $cust_bill_pkg->sdate)
: '' ),
($cust_bill_pkg->edate
- ?time2str("%x", $cust_bill_pkg->edate)
+ ? time2str("%x", $cust_bill_pkg->edate)
: '' ),
);
}
-=item comp
-
-Pays this invoice with a compliemntary payment. If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
sub comp {
- my $self = shift;
- my $cust_pay = new FS::cust_pay ( {
- 'invnum' => $self->invnum,
- 'paid' => $self->owed,
- '_date' => '',
- 'payby' => 'COMP',
- 'payinfo' => $self->cust_main->payinfo,
- 'paybatch' => '',
- } );
- $cust_pay->insert;
+ croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
}
=item realtime_card
}
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;
#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
$self->cust_main->agent_invoice_from;
}
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Returns an text invoice, as a list of lines.
+=item invoice_barcode DIR_OR_FALSE
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
+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.
-I<time>, if specified, is 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
-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_text {
- my $self = shift;
- my( $today, $template, %opt );
- if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
- } else {
- ( $today, $template, %opt ) = @_;
- }
-
- my %params = ( 'format' => 'template' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
-
- $self->print_generic( %params );
-}
-
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-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.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time>, if specified, is 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.
-
-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_latex {
- my $self = shift;
- my( $today, $template, %opt );
- if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
- } else {
- ( $today, $template, %opt ) = @_;
- }
-
- my %params = ( 'format' => 'latex' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
-
- $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";
-
- my $agentnum = $self->cust_main->agentnum;
-
- if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
- print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
- or die "can't write temp file: $!\n";
- } else {
- print $lh $conf->config_binary('logo.eps', $agentnum)
- 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'});
-
-}
-
-=item print_generic OPTION => VALUE ...
-
-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
-
-notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-#what's with all the sprintf('%10.2f')'s in here? will it cause any
-# (alignment in text invoice?) problems to change them all to '%.2f' ?
-# yes: fixed width (dot matrix) text printing will be borked
-sub print_generic {
-
- my( $self, %params ) = @_;
- my $today = $params{today} ? $params{today} : time;
- warn "$me 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 !~ /^(CARD|DCRD|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 "$_", @_ },
- 'summary' => 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\-]+}//;
- s/\\([&])/$1/g;
- $_;
- } @_
- },
- 'coupon' => sub { "" },
- 'summary' => 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 { "" },
- 'summary' => 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', $self->cust_main->agentnum) ) {
-
- my $convert_map = $convert_maps{$format}{'returnaddress'};
- $returnaddress = join( "\n", &$convert_map(
- map { s/( {2,})/'~' x length($1)/eg;
- s/$/\\\\\*/;
- $_
- }
- ( $conf->config('company_name', $self->cust_main->agentnum),
- $conf->config('company_address', $self->cust_main->agentnum),
- )
- )
- );
-
- } else {
-
- 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;
-
- }
-
- my %invoice_data = (
-
- #invoice from info
- 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
- 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
- 'returnaddress' => $returnaddress,
- 'agent' => &$escape_function($cust_main->agent->agent),
-
- #invoice info
- 'invnum' => $self->invnum,
- 'date' => time2str($date_format, $self->_date),
- 'today' => time2str('%b %o, %Y', $today),
- 'terms' => $self->terms,
- 'template' => $template, #params{'template'},
- 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
- 'current_charges' => sprintf("%.2f", $self->charged),
- 'duedate' => $self->due_date2str($rdate_format), #date_format?
-
- #customer info
- 'custnum' => $cust_main->display_custnum,
- 'agent_custid' => &$escape_function($cust_main->agent_custid),
- ( map { $_ => &$escape_function($cust_main->$_()) } qw(
- payname company address1 address2 city state zip fax
- )),
-
- #global config
- 'ship_enable' => $conf->exists('invoice-ship_address'),
- 'unitprices' => $conf->exists('invoice-unitprice'),
- 'smallernotes' => $conf->exists('invoice-smallernotes'),
- 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
- 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
-
- # better hang on to conf_dir for a while (for old templates)
- 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
-
- #these are only used when doing paged plaintext
- 'page' => 1,
- 'total_pages' => 1,
-
- );
-
- $invoice_data{finance_section} = '';
- if ( $conf->config('finance_pkgclass') ) {
- my $pkg_class =
- qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
- $invoice_data{finance_section} = $pkg_class->categoryname;
- }
- $invoice_data{finance_amount} = '0.00';
-
- 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);
- }
- $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 @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{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
- $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
- $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
- $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
-
- my $agentnum = $self->cust_main->agentnum;
-
- my $summarypage = '';
- if ( $conf->exists('invoice_usesummary', $agentnum) ) {
- $summarypage = 1;
- }
- $invoice_data{'summarypage'} = $summarypage;
-
- #do variable substitution in notes, footer, smallfooter
- foreach my $include (qw( notes footer smallfooter coupon )) {
-
- my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
- my @inc_src;
-
- if ( $conf->exists($inc_file, $agentnum)
- && length( $conf->config($inc_file, $agentnum) ) ) {
-
- @inc_src = $conf->config($inc_file, $agentnum);
-
- } else {
-
- $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
- my $convert_map = $convert_maps{$format}{$include};
-
- @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
- s/--\@\]/$delimiters{$format}[1]/g;
- $_;
- }
- &$convert_map( $conf->config($inc_file, $agentnum) );
-
- }
-
- 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";
-
- 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;
- }
-
- $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
-
- $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 ',#XXX should be a config too
- 'html' => $conf->config('money_char') || '$',
- 'template' => '',
- );
- my $other_money_char = $other_money_chars{$format};
- $invoice_data{'dollar'} = $other_money_char;
-
- 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),
- 'summarized' => $summarypage ? 'Y' : '',
- };
- $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
- join(' / ', map { $cust_main->balance_date_range(@$_) }
- $self->_prior_month30s
- )
- if $conf->exists('invoice_include_aging');
-
- my $taxtotal = 0;
- my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
- 'subtotal' => $taxtotal, # adjusted below
- 'summarized' => $summarypage ? 'Y' : '',
- };
- my $tax_weight = _pkg_category($tax_section->{description})
- ? _pkg_category($tax_section->{description})->weight
- : 0;
- $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
- $tax_section->{'sort_weight'} = $tax_weight;
-
-
- my $adjusttotal = 0;
- my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
- 'subtotal' => 0, # adjusted below
- 'summarized' => $summarypage ? 'Y' : '',
- };
- my $adjust_weight = _pkg_category($adjust_section->{description})
- ? _pkg_category($adjust_section->{description})->weight
- : 0;
- $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
- $adjust_section->{'sort_weight'} = $adjust_weight;
-
- my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
- my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
- $invoice_data{'multisection'} = $multisection;
- my $late_sections = [];
- my $extra_sections = [];
- my $extra_lines = ();
- if ( $multisection ) {
- ($extra_sections, $extra_lines) =
- $self->_items_extra_usage_sections($escape_function, $format)
- if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
-
- push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
-
- push @detail_items, @$extra_lines if $extra_lines;
- push @sections,
- $self->_items_sections( $late_sections, # this could stand a refactor
- $summarypage,
- $escape_function,
- $extra_sections,
- $format, #bah
- );
- if ($conf->exists('svc_phone_sections')) {
- my ($phone_sections, $phone_lines) =
- $self->_items_svc_phone_sections($escape_function, $format);
- push @{$late_sections}, @$phone_sections;
- push @detail_items, @$phone_lines;
- }
- }else{
- push @sections, { 'description' => '', 'subtotal' => '' };
- }
-
- unless ( $conf->exists('disable_previous_balance')
- || $conf->exists('previous_balance-summary_only')
- )
- {
-
- foreach my $line_item ( $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'}};
- }
- $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, ['',''];
- }
-
- foreach my $section (@sections, @$late_sections) {
-
- $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
- if ( $invoice_data{finance_section} &&
- $section->{'description'} eq $invoice_data{finance_section} );
-
- $section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $section->{'subtotal'})
- if $multisection;
-
- # begin some normalization
- $section->{'amount'} = $section->{'subtotal'}
- if $multisection;
-
-
- if ( $section->{'description'} ) {
- push @buf, ( [ &$escape_function($section->{'description'}), '' ],
- [ '', '' ],
- );
- }
-
- my $multilocation = scalar($cust_main->cust_location); #too expensive?
- my %options = ();
- $options{'section'} = $section if $multisection;
- $options{'format'} = $format;
- $options{'escape_function'} = $escape_function;
- $options{'format_function'} = sub { () } unless $unsquelched;
- $options{'unsquelched'} = $unsquelched;
- $options{'summary_page'} = $summarypage;
- $options{'skip_usage'} =
- scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
- $options{'multilocation'} = $multilocation;
-
- 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'})
- ],
- [ '', '' ],
- [ '', '' ],
- );
- }
-
- }
-
- $invoice_data{current_less_finance} =
- sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
-
- if ( $multisection && !$conf->exists('disable_previous_balance')
- || $conf->exists('previous_balance-summary_only') )
- {
- unshift @sections, $previous_section if $pr_total;
- }
-
- foreach my $tax ( $self->_items_tax ) {
-
- $taxtotal += $tax->{'amount'};
-
- my $description = &$escape_function( $tax->{'description'} );
- my $amount = sprintf( '%.2f', $tax->{'amount'} );
-
- if ( $multisection ) {
-
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => $description,
- amount => $money. $amount,
- product_code => '',
- section => $tax_section,
- };
-
- } else {
-
- push @total_items, {
- 'total_item' => $description,
- 'total_amount' => $other_money_char. $amount,
- };
-
- }
-
- push @buf,[ $description,
- $money_char. $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 = {};
- my $item = 'Total';
- $item = $conf->config('previous_balance-exclude_from_total')
- || 'Total New Charges'
- if $conf->exists('previous_balance-exclude_from_total');
- my $amount = $self->charged +
- ( $conf->exists('disable_previous_balance') ||
- $conf->exists('previous_balance-exclude_from_total')
- ? 0
- : $pr_total
- );
- $total->{'total_item'} = &$embolden_function($item);
- $total->{'total_amount'} =
- &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
- if ( $multisection ) {
- if ( $adjust_section->{'sort_weight'} ) {
- $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
- sprintf("%.2f", ($self->billing_balance || 0) );
- } else {
- $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
- sprintf('%.2f', $self->charged );
- }
- }else{
- push @total_items, $total;
- }
- push @buf,['','-----------'];
- push @buf,[$item,
- $money_char.
- sprintf( '%10.2f', $amount )
- ];
- 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('trim_len'=>60) ) {
-
- 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 my $credit ( $self->_items_credits('trim_len'=>32) ) {
- push @buf, [ $credit->{'description'}, $money_char.$credit->{'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
- unless $adjust_section->{sort_weight};
- }
-
- {
- my $total;
- $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char. sprintf('%.2f', $summarypage
- ? $self->charged +
- $self->billing_balance
- : $self->owed + $pr_total
- )
- );
- if ( $multisection && !$adjust_section->{sort_weight} ) {
- $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 ) {
- if ($conf->exists('svc_phone_sections')) {
- 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)
- );
- my $last_section = pop @sections;
- $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
- $total->{'total_amount'};
- push @sections, $last_section;
- }
- push @sections, @$late_sections
- if $unsquelched;
- }
-
- my @includelist = ();
- push @includelist, 'summary' if $summarypage;
- foreach my $include ( @includelist ) {
-
- my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
- my @inc_src;
-
- if ( length( $conf->config($inc_file, $agentnum) ) ) {
-
- @inc_src = $conf->config($inc_file, $agentnum);
-
- } else {
-
- $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
- my $convert_map = $convert_maps{$format}{$include};
-
- @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
- s/--\@\]/$delimiters{$format}[1]/g;
- $_;
- }
- &$convert_map( $conf->config($inc_file, $agentnum) );
-
- }
-
- 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";
-
- 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;
- }
-
- $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
-
- $invoice_data{$include} =~ s/\n+$//
- if ($format eq 'latex');
- }
-
- $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);
- }
-}
-
-# helper routine for generating date ranges
-sub _prior_month30s {
- my $self = shift;
- my @ranges = (
- [ 1, 2592000 ], # 0-30 days ago
- [ 2592000, 5184000 ], # 30-60 days ago
- [ 5184000, 7776000 ], # 60-90 days ago
- [ 7776000, 0 ], # 90+ days ago
- );
-
- map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
- $_->[1] ? $self->_date - $_->[1] - 1 : '',
- ] }
- @ranges;
-}
-
-=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an postscript invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<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.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_ps {
- my $self = shift;
-
- my ($file, $lfile) = $self->print_latex(@_);
- my $ps = generate_ps($file);
- unlink($lfile);
-
- $ps;
-}
-
-=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an PDF invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<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.
-
-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_pdf {
- my $self = shift;
-
- my ($file, $lfile) = $self->print_latex(@_);
- my $pdf = generate_pdf($file);
- unlink($lfile);
-
- $pdf;
-}
-
-=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
-
-Returns an HTML invoice, as a scalar.
-
-I<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.
-
-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)
-
-I<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, '}',
- '--@]';
- #' doh, gvim
- } 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
- return $self->invoice_terms if $self->invoice_terms;
-
- #check for a customer- specific override
- my $cust_main = $self->cust_main;
- return $cust_main->invoice_terms if $cust_main->invoice_terms;
-
- #use configured default
- $conf->config('invoice_default_terms') || '';
-}
-
-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($date_format);
- } 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($rdate_format, $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. ' ('. $self->_date_pretty. ')';
-}
-
-=item _date_pretty
-
-Returns a string with the date, for example: "3/20/2008"
-
-=cut
-
-sub _date_pretty {
- my $self = shift;
- time2str($date_format, $self->_date);
-}
-
-use vars qw(%pkg_category_cache);
-sub _items_sections {
- my $self = shift;
- my $late = shift;
- my $summarypage = shift;
- my $escape = shift;
- my $extra_sections = shift;
- my $format = shift;
-
- my %subtotal = ();
- my %late_subtotal = ();
- my %not_tax = ();
-
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
- {
-
- my $usage = $cust_bill_pkg->usage;
-
- foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
- next if ( $display->summary && $summarypage );
-
- my $section = $display->section;
- my $type = $display->type;
-
- $not_tax{$section} = 1
- unless $cust_bill_pkg->pkgnum == 0;
-
- if ( $display->post_total && !$summarypage ) {
- if (! $type || $type eq 'S') {
- $late_subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0;
- }
-
- if (! $type) {
- $late_subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0;
- }
-
- if ($type && $type eq 'R') {
- $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0;
- }
-
- if ($type && $type eq 'U') {
- $late_subtotal{$section} += $usage
- unless scalar(@$extra_sections);
- }
-
- } else {
-
- next if $cust_bill_pkg->pkgnum == 0 && ! $section;
-
- if (! $type || $type eq 'S') {
- $subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0;
- }
-
- if (! $type) {
- $subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0;
- }
-
- if ($type && $type eq 'R') {
- $subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0;
- }
-
- if ($type && $type eq 'U') {
- $subtotal{$section} += $usage
- unless scalar(@$extra_sections);
- }
-
- }
-
- }
-
- }
-
- %pkg_category_cache = ();
-
- push @$late, map { { 'description' => &{$escape}($_),
- 'subtotal' => $late_subtotal{$_},
- 'post_total' => 1,
- 'sort_weight' => ( _pkg_category($_)
- ? _pkg_category($_)->weight
- : 0
- ),
- ((_pkg_category($_) && _pkg_category($_)->condense)
- ? $self->_condense_section($format)
- : ()
- ),
- } }
- sort _sectionsort keys %late_subtotal;
-
- my @sections;
- if ( $summarypage ) {
- @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
- map { $_->categoryname } qsearch('pkg_category', {});
- } else {
- @sections = keys %subtotal;
- }
-
- my @early = map { { 'description' => &{$escape}($_),
- 'subtotal' => $subtotal{$_},
- 'summarized' => $not_tax{$_} ? '' : 'Y',
- 'tax_section' => $not_tax{$_} ? '' : 'Y',
- 'sort_weight' => ( _pkg_category($_)
- ? _pkg_category($_)->weight
- : 0
- ),
- ((_pkg_category($_) && _pkg_category($_)->condense)
- ? $self->_condense_section($format)
- : ()
- ),
- }
- } @sections;
- push @early, @$extra_sections if $extra_sections;
-
- sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
-
-}
-
-#helper subs for above
-
-sub _sectionsort {
- _pkg_category($a)->weight <=> _pkg_category($b)->weight;
-}
-
-sub _pkg_category {
- my $categoryname = shift;
- $pkg_category_cache{$categoryname} ||=
- qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
-}
-
-my %condensed_format = (
- 'label' => [ qw( Description Qty Amount ) ],
- 'fields' => [
- sub { shift->{description} },
- sub { shift->{quantity} },
- sub { shift->{amount} },
- ],
- 'align' => [ qw( l r r ) ],
- 'span' => [ qw( 5 1 1 ) ], # unitprices?
- 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
-);
-
-sub _condense_section {
- my ( $self, $format ) = ( shift, shift );
- ( 'condensed' => 1,
- map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
- qw( description_generator
- header_generator
- total_generator
- total_line_generator
- )
- );
-}
-
-sub _condensed_generator_defaults {
- my ( $self, $format ) = ( shift, shift );
- return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
-}
-
-my %html_align = (
- 'c' => 'center',
- 'l' => 'left',
- 'r' => 'right',
-);
-
-sub _condensed_header_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
-
- if ($format eq 'latex') {
- $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
- $suffix = "\\\\\n\\hline";
- $separator = "&\n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
- };
- } elsif ( $format eq 'html' ) {
- $prefix = '<th></th>';
- $suffix = '';
- $separator = '';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<th align="$html_align{$a}">$d</th>!;
- };
- }
-
- sub {
- my @args = @_;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
- }
-
- $prefix. join($separator, @result). $suffix;
- };
-
-}
-
-sub _condensed_description_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
-
- if ($format eq 'latex') {
- $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
- $suffix = '\\\\';
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
- };
- }elsif ( $format eq 'html' ) {
- $prefix = '"><td align="center"></td>';
- $suffix = '';
- $separator = '';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}">$d</td>!;
- };
- }
-
- sub {
- my @args = @_;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
- map { $f->{$_}->[$i] } qw(align span width)
- );
+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;
}
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
+ return $gd->png;
}
-sub _condensed_total_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
- my $style = '';
-
- if ($format eq 'latex') {
- $prefix = "& ";
- $suffix = "\\\\\n";
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
- };
- }elsif ( $format eq 'html' ) {
- $prefix = '';
- $suffix = '';
- $separator = '';
- $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
- };
- }
-
-
- sub {
- my @args = @_;
- my @result = ();
-
- # my $r = &{$f->{fields}->[$i]}(@args);
- # $r .= ' Total' unless $i;
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
- map { $f->{$_}->[$i] } qw(align span width)
- );
- }
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
-}
+=item invnum_date_pretty
-=item total_line_generator FORMAT
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)".
-Returns a coderef used for generation of invoice total line items for this
-usage_class. FORMAT is either html or latex
+Intended for back-end context, with regard to translation and date formatting.
=cut
-# should not be used: will have issues with hash element names (description vs
-# total_item and amount vs total_amount -- another array of functions?
-
-sub _condensed_total_line_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
- my $style = '';
-
- if ($format eq 'latex') {
- $prefix = "& ";
- $suffix = "\\\\\n";
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
- };
- }elsif ( $format eq 'html' ) {
- $prefix = '';
- $suffix = '';
- $separator = '';
- $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
- };
- }
-
-
- sub {
- my @args = @_;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( &{$f->{fields}->[$i]}(@args),
- map { $f->{$_}->[$i] } qw(align span width)
- );
- }
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
+#note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
+# for backend use (and also does the wrong thing, localizing for end customer
+# instead of backoffice configured date format)
+sub invnum_date_pretty {
+ my $self = shift;
+ #$self->mt('Invoice #').
+ 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+ $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
}
#sub _items_extra_usage_sections {
sub _items_extra_usage_sections {
my $self = shift;
+ my $conf = $self->conf;
my $escape = shift;
my $format = shift;
my %classnums = ();
my %lines = ();
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
+
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
next unless $cust_bill_pkg->pkgnum > 0;
my $desc = $detail->regionname;
my $description = $desc;
- $description = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
$lines{$section}{$desc} ||= {
description => &{$escape}($description),
}
+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
+ local($FS::Record::qsearch_qualify_columns) = 0;
+ my $phone_portedin = qsearchs( 'h_svc_phone',
+ { 'svcnum' => $h_cust_svc->svcnum,
+ 'lnp_status' => 'portedin' },
+ FS::h_svc_phone->sql_h_searchs($end),
+ );
+ $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;
+ }
+
+ # don't look at this service again
+ push @seen, $h_cust_svc->svcnum;
+ }
+ }
+
+ $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 = ();
+
+ 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);
+ $section->{'header'} = join(',',@header);
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+ $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;
+ }
+ }
+
+ 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;
+ }
+
+ my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
+
+ return ($section,\@sorted_lines);
+}
+
sub _items_svc_phone_sections {
my $self = shift;
+ my $conf = $self->conf;
my $escape = shift;
my $format = shift;
my %classnums = ();
my %lines = ();
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
+
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
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;
my $desc = $detail->regionname;
my $description = $desc;
- $description = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
$lines{$phonenum}{$desc} ||= {
description => &{$escape}($description),
'duration' => 0,
'sort_weight' => $usage_class{$detail->classnum}->weight,
'phonenum' => $phonenum,
+ 'header' => [ @header ],
};
$sections{"$phonenum $line"}{amount} += $amount; #subtotal
$sections{"$phonenum $line"}{calls}++;
my %sectionmap = ();
my $simple = new FS::usage_class { format => 'simple' }; #bleh
- my $usage_simple = new FS::usage_class { format => 'usage_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},
'sort_weight' => $sections{$_}{sort_weight},
'post_total' => $summary, #inspire pagebreak
(
- ( map { $_ => $usage_class->$_($format) }
+ ( map { $_ => $usage_class->$_($format, %gen_opt) }
qw( description_generator
header_generator
total_generator
push @lines, $l;
}
}
+
+ 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;
+ }
+ }
+
+ # 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;
+ }
+ }
+ }
+
+ # 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;
+ }
+ }
+ }
+
+ return(\@newsections, \@newlines);
+ }
return(\@sections, \@lines);
}
-sub _items {
- my $self = shift;
+=sub _items_usage_class_summary OPTIONS
- #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 );
+Returns a list of detail items summarizing the usage charges on this
+invoice. Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
- my @b = ();
- foreach my $display ( @display ) {
- push @b, $self->$display(@_);
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
+ my $self = shift;
+ my %opt = @_;
+
+ my $escape = $opt{escape} || sub { $_[0] };
+ my $invnum = $self->invnum;
+ my @classes = qsearch({
+ 'table' => 'usage_class',
+ 'select' => 'classnum, classname, SUM(amount) AS amount',
+ 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+ ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+ 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+ ' GROUP BY classnum, classname, weight'.
+ ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+ ' ORDER BY weight ASC',
+ });
+ my @l;
+ my $section = {
+ description => &{$escape}($self->mt('Usage Summary')),
+ no_subtotal => 1,
+ usage_section => 1,
+ };
+ foreach my $class (@classes) {
+ push @l, {
+ 'description' => &{$escape}($class->classname),
+ 'amount' => sprintf('%.2f', $class->amount),
+ 'usage_classnum' => $class->classnum,
+ 'section' => $section,
+ };
}
- @b;
+ return @l;
}
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);
+ ? 'due '. $_->due_date2str('short')
+ : $self->time2str_local('short', $_->_date);
push @b, {
- 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($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 @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
- my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
- if ($options{section} && $options{section}->{condensed}) {
- my %itemshash = ();
- local $Storable::canonical = 1;
- foreach ( @items ) {
- my $item = { %$_ };
- delete $item->{ref};
- delete $item->{ext_description};
- my $key = freeze($item);
- $itemshash{$key} ||= 0;
- $itemshash{$key} ++; # += $item->{quantity};
- }
- @items = sort { $a->{description} cmp $b->{description} }
- map { my $i = thaw($_);
- $i->{quantity} = $itemshash{$_};
- $i->{amount} =
- sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
- $i;
- }
- keys %itemshash;
- }
- @items;
-}
-
-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 $summary_page = $opt{summary_page} || '';
- my $multilocation = $opt{multilocation} || '';
-
- my @b = ();
- my ($s, $r, $u) = ( undef, undef, undef );
- foreach my $cust_bill_pkg ( @$cust_bill_pkg )
- {
-
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
- if ( $_ && !$cust_bill_pkg->hidden ) {
- $_->{amount} = sprintf( "%.2f", $_->{amount} ),
- $_->{amount} =~ s/^\-0\.00$/0.00/;
- $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ }
- unless $_->{amount} == 0;
- $_ = undef;
- }
- }
-
- foreach my $display ( grep { defined($section)
- ? $_->section eq $section
- : 1
- }
- grep { !$_->summary || !$summary_page }
- $cust_bill_pkg->cust_bill_pkg_display
- )
- {
-
- my $type = $display->type;
-
- my $desc = $cust_bill_pkg->desc;
- $desc = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
-
- my %details_opt = ( 'format' => $format,
- 'escape_function' => $escape_function,
- 'format_function' => $format_function,
- );
-
- if ( $cust_bill_pkg->pkgnum > 0 ) {
-
- my $cust_pkg = $cust_bill_pkg->cust_pkg;
-
- if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
-
- my $description = $desc;
- $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-
- my @d = ();
- unless ( $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->hidden )
- {
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date);
- if ( $multilocation ) {
- my $loc = $cust_pkg->location_label;
- $loc = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($loc) > 50;
- push @d, &{$escape_function}($loc);
- }
- }
- push @d, $cust_bill_pkg->details(%details_opt)
- if $cust_bill_pkg->recur == 0;
-
- if ( $cust_bill_pkg->hidden ) {
- $s->{amount} += $cust_bill_pkg->setup;
- $s->{unit_amount} += $cust_bill_pkg->unitsetup;
- push @{ $s->{ext_description} }, @d;
- } else {
- $s = {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $cust_bill_pkg->setup,
- unit_amount => $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 && $type && $type eq 'U')
- ? "Usage charges" : $desc;
-
- unless ( $conf->exists('disable_line_item_date_ranges') ) {
- $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
- " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
- }
-
- my @d = ();
-
- #at least until cust_bill_pkg has "past" ranges in addition to
- #the "future" sdate/edate ones... see #3032
- my @dates = ( $self->_date );
- my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
- push @dates, $prev->sdate if $prev;
-
- unless ( $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->itemdesc
- || $cust_bill_pkg->hidden
- || $is_summary && $type && $type eq 'U' )
- {
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short(@dates)
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate)
- ;
- if ( $multilocation ) {
- my $loc = $cust_pkg->location_label;
- $loc = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($loc) > 50;
- push @d, &{$escape_function}($loc);
- }
- }
-
- 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;
- }
-
- if ( !$type || $type eq 'R' ) {
-
- if ( $cust_bill_pkg->hidden ) {
- $r->{amount} += $amount;
- $r->{unit_amount} += $cust_bill_pkg->unitrecur;
- push @{ $r->{ext_description} }, @d;
- } else {
- $r = {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $amount,
- unit_amount => $cust_bill_pkg->unitrecur,
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- };
- }
-
- } elsif ( $amount ) { # && $type eq 'U'
-
- if ( $cust_bill_pkg->hidden ) {
- $u->{amount} += $amount;
- $u->{unit_amount} += $cust_bill_pkg->unitrecur;
- push @{ $u->{ext_description} }, @d;
- } else {
- $u = {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $amount,
- unit_amount => $cust_bill_pkg->unitrecur,
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- };
- }
-
- }
-
- } # recurring or usage with recurring charge
-
- } 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($date_format, $cust_bill_pkg->sdate). ' - '.
- time2str($date_format, $cust_bill_pkg->edate). ')',
- 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
- };
- }
-
- }
-
- }
-
- }
-
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
- if ( $_ ) {
- $_->{amount} = sprintf( "%.2f", $_->{amount} ),
- $_->{amount} =~ s/^\-0\.00$/0.00/;
- $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ }
- unless $_->{amount} == 0;
- }
- }
-
- @b;
-
-}
-
sub _items_credits {
my( $self, %opt ) = @_;
- my $trim_len = $opt{'trim_len'} || 60;
+ my $trim_len = $opt{'trim_len'} || 40;
my @b;
#credits
- foreach ( $self->cust_credited ) {
+ my @objects;
+ if ( $self->conf->exists('previous_balance-payments_since') ) {
+ if ( $opt{'template'} eq 'statement' ) {
+ # then the current bill is a "statement" (i.e. an invoice sent as
+ # a payment receipt)
+ # and in that case we want to see payments on or after THIS invoice
+ @objects = qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $self->_date},
+ });
+ } else {
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $date},
+ });
+ }
+ } else {
+ @objects = $self->cust_credited;
+ }
- #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+ foreach my $obj ( @objects ) {
+ my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
- my $reason = substr($_->cust_credit->reason, 0, $trim_len);
- $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($date_format,$_->cust_credit->_date). $reason,
- 'amount' => sprintf("%.2f",$_->amount),
+ 'description' => $self->mt('Credit applied').' '.
+ $self->time2str_local('short', $obj->_date). $reason,
+ 'amount' => sprintf("%.2f",$obj->amount),
};
}
sub _items_payments {
my $self = shift;
+ my %opt = @_;
my @b;
- #get & print payments
- foreach ( $self->cust_bill_pay ) {
+ my $detailed = $self->conf->exists('invoice_payment_details');
+ my @objects;
+ if ( $self->conf->exists('previous_balance-payments_since') ) {
+ # then show payments dated on/after the previous bill...
+ if ( $opt{'template'} eq 'statement' ) {
+ # then the current bill is a "statement" (i.e. an invoice sent as
+ # a payment receipt)
+ # and in that case we want to see payments on or after THIS invoice
+ @objects = qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $self->_date},
+ });
+ } else {
+ # the normal case: payments on or after the previous invoice
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $date},
+ });
+ # and before the current bill...
+ @objects = grep { $_->_date < $self->_date } @objects;
+ }
+ } else {
+ @objects = $self->cust_bill_pay;
+ }
- #something more elaborate if $_->amount ne ->cust_pay->paid ?
+ foreach my $obj (@objects) {
+ my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
+ my $desc = $self->mt('Payment received').' '.
+ $self->time2str_local('short', $cust_pay->_date );
+ $desc .= $self->mt(' via ') .
+ $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
+ if $detailed;
push @b, {
- 'description' => "Payment received ".
- time2str($date_format,$_->cust_pay->_date ),
- 'amount' => sprintf("%.2f", $_->amount )
+ 'description' => $desc,
+ 'amount' => sprintf("%.2f", $obj->amount )
};
}
}
+sub _items_total {
+ my $self = shift;
+ my $conf = $self->conf;
+
+ my @items;
+ my ($pr_total) = $self->previous;
+ my ($new_charges_desc, $new_charges_amount);
+
+ if ( $conf->exists('previous_balance-exclude_from_total') ) {
+ # then return separate lines for previous balance and total new charges
+ if ( $pr_total ) {
+ push @items,
+ { total_item => $self->mt('Previous Balance'),
+ total_amount => sprintf('%.2f',$pr_total)
+ };
+ }
+ $new_charges_desc = $self->mt(
+ $conf->config('previous_balance-exclude_from_total')
+ || 'Total New Charges'
+ ); # localize 'Total New Charges' or whatever's in the config
+
+ $new_charges_amount = $self->charged;
+
+ } else {
+
+ $new_charges_desc = $self->mt('Total Charges');
+ $new_charges_amount = sprintf('%.2f',$self->charged + $pr_total);
+
+ }
+
+ if ( $conf->exists('invoice_show_prior_due_date') ) {
+ # then the due date should be shown with Total New Charges,
+ # and should NOT be shown with the Balance Due message.
+ if ( $self->due_date ) {
+ # localize the "Please pay by" message and the date itself
+ # (grammar issues with this, yeah)
+ $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
+ $self->due_date2str('short');
+ } elsif ( $self->terms ) {
+ # phrases like "due on receipt" should be localized
+ $new_charges_desc .= ' - ' . $self->mt($self->terms);
+ }
+ }
+
+ push @items,
+ { total_item => $new_charges_desc,
+ total_amount => $new_charges_amount,
+ };
+
+ @items;
+}
+
+
+
=item call_details [ OPTION => VALUE ... ]
Returns an array of CSV strings representing the call details for this invoice
process_re_X('spool', @_);
}
-use Storable qw(thaw);
use Data::Dumper;
-use MIME::Base64;
sub process_re_X {
my( $method, $job ) = ( shift, shift );
warn "$me process_re_X $method for job $job\n" if $DEBUG;
- my $param = thaw(decode_base64(shift));
+ my $param = shift;
warn Dumper($param) if $DEBUG;
re_X(
}
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".
}
+sub API_getinfo {
+ my $self = shift;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+ 'owed' => $self->owed,
+ #XXX last payment applied date
+ };
+}
+
=back
=head1 CLASS METHODS
=cut
sub owed_sql {
- my $class = shift;
- my ($start, $end) = @_;
+ my ($class, $start, $end) = @_;
'charged - '.
$class->paid_sql($start, $end). ' - '.
$class->credited_sql($start, $end);
=cut
sub net_sql {
- my $class = shift;
- my ($start, $end) = @_;
+ my ($class, $start, $end) = @_;
'charged - '. $class->credited_sql($start, $end);
}
sub paid_sql {
my ($class, $start, $end) = @_;
$start &&= "AND cust_bill_pay._date <= $start";
- $end &&= "AND cust_bill_pay._date > $end";
+ $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 $start $end )";
}
=cut
sub credited_sql {
- my ($class, $start, $end) = shift;
+ my ($class, $start, $end) = @_;
$start &&= "AND cust_credit_bill._date <= $start";
- $end &&= "AND cust_credit_bill._date > $end";
+ $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 $start $end )";
}
-=item search_sql_where HASHREF
-
-Class method which returns an SQL WHERE fragment to search for parameters
-specified in HASHREF. Valid parameters are
-
-=over 4
-
-=item _date
-
-List reference of start date, end date, as UNIX timestamps.
-
-=item invnum_min
-
-=item invnum_max
-
-=item agentnum
-
-=item charged
-
-List reference of charged limits (exclusive).
-
-=item owed
-
-List reference of charged limits (exclusive).
-
-=item open
-
-flag, return open invoices only
-
-=item net
-
-flag, return net invoices only
-
-=item days
-
-=item newest_percust
+=item due_date_sql
-=back
-
-Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
+Returns an SQL fragment to retrieve the due date of an invoice.
+Currently only supported on PostgreSQL.
=cut
-sub search_sql_where {
- my($class, $param) = @_;
- if ( $DEBUG ) {
- warn "$me search_sql_where called with params: \n".
- join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
- }
-
- my @search = ();
-
- #agentnum
- if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.agentnum = $1";
- }
-
- #_date
- if ( $param->{_date} ) {
- my($beginning, $ending) = @{$param->{_date}};
-
- push @search, "cust_bill._date >= $beginning",
- "cust_bill._date < $ending";
- }
-
- #invnum
- if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill.invnum >= $1";
- }
- if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill.invnum <= $1";
- }
-
- #charged
- if ( $param->{charged} ) {
- my @charged = ref($param->{charged})
- ? @{ $param->{charged} }
- : ($param->{charged});
-
- push @search, map { s/^charged/cust_bill.charged/; $_; }
- @charged;
- }
-
- my $owed_sql = FS::cust_bill->owed_sql;
-
- #owed
- if ( $param->{owed} ) {
- my @owed = ref($param->{owed})
- ? @{ $param->{owed} }
- : ($param->{owed});
- push @search, map { s/^owed/$owed_sql/; $_; }
- @owed;
- }
-
- #open/net flags
- push @search, "0 != $owed_sql"
- if $param->{'open'};
- push @search, '0 != '. FS::cust_bill->net_sql
- if $param->{'net'};
-
- #days
- push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
- if $param->{'days'};
-
- #newest_percust
- if ( $param->{'newest_percust'} ) {
-
- #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
- #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
-
- my @newest_where = map { my $x = $_;
- $x =~ s/\bcust_bill\./newest_cust_bill./g;
- $x;
- }
- grep ! /^cust_main./, @search;
- my $newest_where = scalar(@newest_where)
- ? ' AND '. join(' AND ', @newest_where)
- : '';
-
-
- push @search, "cust_bill._date = (
- SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
- WHERE newest_cust_bill.custnum = cust_bill.custnum
- $newest_where
- )";
-
- }
-
- #agent virtualization
- my $curuser = $FS::CurrentUser::CurrentUser;
- if ( $curuser->username eq 'fs_queue'
- && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
- my $username = $1;
- my $newuser = qsearchs('access_user', {
- 'username' => $username,
- 'disabled' => '',
- } );
- if ( $newuser ) {
- $curuser = $newuser;
- } else {
- warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
- }
- }
- push @search, $curuser->agentnums_sql;
+sub due_date_sql {
+ die "don't use: doesn't account for agent-specific invoice_default_terms";
- join(' AND ', @search );
+ #we're passed a $conf but not a specific customer (that's in the query), so
+ # to make this work we'd need an agentnum-aware "condition_sql_conf" like
+ # "condition_sql_option" that retreives a conf value with SQL in an agent-
+ # aware fashion
+ my $conf = new FS::Conf;
+'COALESCE(
+ SUBSTRING(
+ COALESCE(
+ cust_bill.invoice_terms,
+ cust_main.invoice_terms,
+ \''.($conf->config('invoice_default_terms') || '').'\'
+ ), E\'Net (\\\\d+)\'
+ )::INTEGER, 0
+) * 86400 + cust_bill._date'
}
=back