package FS::cust_bill;
use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
+use vars qw( @ISA $DEBUG $me $conf
+ $money_char $date_format $rdate_format $date_format_long );
use vars qw( $invoice_lines @buf ); #yuck
use Fcntl qw(:flock); #for spool_csv
+use Cwd;
use List::Util qw(min max);
use Date::Format;
use Text::Template 1.20;
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::Record qw( qsearch qsearchs dbh );
use FS::cust_main_Mixin;
use FS::cust_main;
+use FS::cust_statement;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_detail;
use FS::cust_credit;
use FS::cust_pay;
use FS::cust_pkg;
use FS::cust_bill_pay_batch;
use FS::part_bill_event;
use FS::payby;
+use FS::bill_batch;
+use FS::cust_bill_batch;
+use FS::cust_bill_pay_pkg;
+use FS::cust_credit_bill_pkg;
@ISA = qw( FS::cust_main_Mixin FS::Record );
#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') || '$';
+ $money_char = $conf->config('money_char') || '$';
+ $date_format = $conf->config('date_format') || '%x'; #/YY
+ $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
+ $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
} );
=head1 NAME
(see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
following fields are currently supported:
+Regular fields
+
=over 4
=item invnum - primary key (assigned automatically for new invoices)
=item charged - amount of this invoice
+=item invoice_terms - optional terms override for this specific invoice
+
+=back
+
+Customer info at invoice generation time
+
+=over 4
+
+=item previous_balance
+
+=item billing_balance
+
+=back
+
+Deprecated
+
+=over 4
+
=item printed - deprecated
+=back
+
+Specific use cases
+
+=over 4
+
=item closed - books closed flag, empty or `Y'
+=item statementnum - invoice aggregation (see L<FS::cust_statement>)
+
+=item agent_invid - legacy invoice number
+
=back
=head1 METHODS
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 delete
This method now works but you probably shouldn't use it. Instead, apply a
sub delete {
my $self = shift;
return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
- $self->SUPER::delete(@_);
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ foreach my $table (qw(
+ cust_bill_event
+ cust_event
+ cust_credit_bill
+ cust_bill_pay
+ cust_bill_pay
+ cust_credit_bill
+ cust_pay_batch
+ cust_bill_pay_batch
+ cust_bill_pkg
+ )) {
+
+ foreach my $linked ( $self->$table() ) {
+ my $error = $linked->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ my $error = $self->SUPER::delete(@_);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';
+
}
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ]
-Replaces the OLD_RECORD with this one in the database. If there is an error,
-returns the error, otherwise returns false.
+You can, but probably shouldn't modify invoices...
-Only printed may be changed. printed is normally updated by calling the
-collect method of a customer object (see L<FS::cust_main>).
+Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
+supplied, replaces this record. If there is an error, returns the error,
+otherwise returns false.
=cut
sub replace_check {
my( $new, $old ) = ( shift, shift );
- return "Can't change custnum!" unless $old->custnum == $new->custnum;
+ return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
#return "Can't change _date!" unless $old->_date eq $new->_date;
- return "Can't change _date!" unless $old->_date == $new->_date;
- return "Can't change charged!" unless $old->charged == $new->charged
- || $old->charged == 0;
+ return "Can't change _date" unless $old->_date == $new->_date;
+ return "Can't change charged" unless $old->charged == $new->charged
+ || $old->charged == 0
+ || $new->{'Hash'}{'cc_surcharge_replace_hack'};
'';
}
+
+=item add_cc_surcharge
+
+Giant hack
+
+=cut
+
+sub add_cc_surcharge {
+ my ($self, $pkgnum, $amount) = (shift, shift, shift);
+
+ my $error;
+ my $cust_bill_pkg = new FS::cust_bill_pkg({
+ 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ 'setup' => $amount,
+ });
+ $error = $cust_bill_pkg->insert;
+ return $error if $error;
+
+ $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
+ $self->charged($self->charged+$amount);
+ $error = $self->replace;
+ return $error if $error;
+
+ $self->apply_payments_and_credits;
+}
+
+
=item check
Checks all fields to make sure this is a valid invoice. If there is an error,
my $error =
$self->ut_numbern('invnum')
- || $self->ut_number('custnum')
+ || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
|| $self->ut_numbern('_date')
|| $self->ut_money('charged')
|| $self->ut_numbern('printed')
|| $self->ut_enum('closed', [ '', 'Y' ])
+ || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
+ || $self->ut_numbern('agent_invid') #varchar?
;
return $error if $error;
- return "Unknown customer"
- unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-
$self->_date(time) unless $self->_date;
$self->printed(0) if $self->printed eq '';
$self->SUPER::check;
}
+=item display_invnum
+
+Returns the displayed invoice number for this invoice: agent_invid if
+cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
+
+=cut
+
+sub display_invnum {
+ my $self = shift;
+ if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ return $self->agent_invid;
+ } else {
+ return $self->invnum;
+ }
+}
+
=item previous
Returns a list consisting of the total previous balance for this customer,
sub cust_pkg {
my $self = shift;
- my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+ my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+ $self->cust_bill_pkg;
my %saw = ();
grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
}
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+ my $self = shift;
+ grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
=item open_cust_bill_pkg
Returns the open line items for this invoice.
#;
}
+sub cust_pay_batch {
+ my $self = shift;
+ qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
+}
+
+sub cust_bill_pay_batch {
+ my $self = shift;
+ qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+}
+
=item cust_bill_pay
Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
sub cust_bill_pay {
my $self = shift;
+ map { $_ } #return $self->num_cust_bill_pay unless wantarray;
sort { $a->_date <=> $b->_date }
qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
}
=item cust_credited
+=item cust_credit_bill
+
Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
=cut
sub cust_credited {
my $self = shift;
+ map { $_ } #return $self->num_cust_credit_bill unless wantarray;
sort { $a->_date <=> $b->_date }
qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
;
}
-=item cust_bill_pay_pkgnum
+sub cust_credit_bill {
+ shift->cust_credited(@_);
+}
+
+#=item cust_bill_pay_pkgnum PKGNUM
+#
+#Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_bill_pay_pkgnum {
+# my( $self, $pkgnum ) = @_;
+# map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
+# sort { $a->_date <=> $b->_date }
+# qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+# 'pkgnum' => $pkgnum,
+# }
+# );
+#}
+
+=item cust_bill_pay_pkg PKGNUM
Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
-with matching pkgnum.
+applied against the matching pkgnum.
=cut
-sub cust_bill_pay_pkgnum {
+sub cust_bill_pay_pkg {
my( $self, $pkgnum ) = @_;
- 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 ) ',
+ 'hashref' => { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ },
+ });
+
}
-=item cust_credited_pkgnum
+#=item cust_credited_pkgnum PKGNUM
+#
+#=item cust_credit_bill_pkgnum PKGNUM
+#
+#Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_credited_pkgnum {
+# my( $self, $pkgnum ) = @_;
+# map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
+# sort { $a->_date <=> $b->_date }
+# qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+# 'pkgnum' => $pkgnum,
+# }
+# );
+#}
+#
+#sub cust_credit_bill_pkgnum {
+# shift->cust_credited_pkgnum(@_);
+#}
+
+=item cust_credit_bill_pkg PKGNUM
-Returns all 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 ) = @_;
- 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 ) ',
+ 'hashref' => { 'invnum' => $self->invnum,
+ 'pkgnum' => $pkgnum,
+ },
+ });
+
}
=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 apply_payments_and_credits
+=item apply_payments_and_credits [ OPTION => VALUE ... ]
+
+Applies unapplied payments and credits to this invoice.
+
+A hash of optional arguments may be passed. Currently "manual" is supported.
+If true, a payment receipt is sent instead of a statement when
+'payment_receipt_email' configuration option is set.
+
+If there is an error, returns the error, otherwise returns false.
=cut
sub apply_payments_and_credits {
- my $self = shift;
+ my( $self, %options ) = @_;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
}
next unless $owed > 0;
- warn "min ( $unapp_amount, $owed )\n";
+ warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
$app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
$app->invnum( $self->invnum );
- my $error = $app->insert;
+ my $error = $app->insert(%options);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error inserting ". $app->table. " record: $error";
email subject, optional
+=item notice_name
+
+notice name instead of "Invoice", optional
+
=back
Returns an argument list to be passed to L<FS::Misc::send_email>.
'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
);
- my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
+ my %opt = (
+ 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+ 'template' => $args{'template'},
+ 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
+ 'no_coupon' => $args{'no_coupon'},
+ );
+
+ my $cust_main = $self->cust_main;
if (ref($args{'to'}) eq 'ARRAY') {
$return{'to'} = $args{'to'};
} else {
$return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
- $self->cust_main->invoicing_list
+ $cust_main->invoicing_list
];
}
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$data = $args{'print_text'};
} else {
- $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
+ $data = [ $self->print_text(\%opt) ];
}
}
my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
my $logo;
- my $agentnum = $self->cust_main->agentnum;
+ my $agentnum = $cust_main->agentnum;
if ( defined($args{'template'}) && length($args{'template'})
&& $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
)
'Filename' => 'logo.png',
'Content-ID' => "<$content_id>",
;
+
+ my $barcode;
+ if($conf->exists('invoice-barcode')){
+ my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
+ $barcode = build MIME::Entity
+ 'Type' => 'image/png',
+ 'Encoding' => 'base64',
+ 'Data' => $self->invoice_barcode(0),
+ 'Filename' => 'barcode.png',
+ 'Content-ID' => "<$barcode_content_id>",
+ ;
+ $opt{'barcode_cid'} = $barcode_content_id;
+ }
$alternative->attach(
'Type' => 'text/html',
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- $self->print_html({ time => '',
- template => $args{'template'},
- cid => $content_id,
- %cdrs,
- }),
+ $self->print_html({ 'cid'=>$content_id, %opt }),
' </body>',
'</html>',
],
);
my @otherparts = ();
- if ( $self->cust_main->email_csv_cdr ) {
+ if ( $cust_main->email_csv_cdr ) {
push @otherparts, build MIME::Entity
'Type' => 'text/csv',
'Encoding' => '7bit',
- 'Data' => [ map { "$_\n" } $self->call_details ],
+ 'Data' => [ map { "$_\n" }
+ $self->call_details('prepend_billed_number' => 1)
+ ],
'Disposition' => 'attachment',
+ 'Filename' => 'usage-'. $self->invnum. '.csv',
;
}
$related->add_part($image);
- my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
+ my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
$return{'mimeparts'} = [ $related, $pdf, @otherparts ];
# image/png
$return{'content-type'} = 'multipart/related';
- $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
+ if($conf->exists('invoice-barcode')){
+ $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
+ }
+ else {
+ $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
+ }
$return{'type'} = 'multipart/alternative'; #Content-Type of first part...
#$return{'disposition'} = 'inline';
#mime parts arguments a la MIME::Entity->build().
$return{'mimeparts'} = [
- { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
+ { $self->mimebuild_pdf(\%opt) }
];
}
if ( ref($args{'print_text'}) eq 'ARRAY' ) {
$return{'body'} = $args{'print_text'};
} else {
- $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
+ $return{'body'} = [ $self->print_text(\%opt) ];
}
}
);
}
-=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
Sends this invoice to the destinations configured for this customer: sends
email, prints and/or faxes. See L<FS::cust_main_invoice>.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to
+four values for templatename, agentnum, invoice_from and amount.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
-AGENTNUM, if specified, means that this invoice will only be sent for customers
+I<agentnum>, if specified, means that this invoice will only be sent for customers
of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
single agent) or an arrayref of agentnums.
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From: address.
-AMOUNT, if specified, only sends the invoice if the total amount owed on this
+I<amount>, if specified, only sends the invoice if the total amount owed on this
invoice and all older invoices is greater than the specified amount.
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
=cut
sub queueable_send {
sub send {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
- if ( scalar(@_) && $_[0] ) {
- my $agentnums = ref($_[0]) ? shift : [ shift ];
- return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
- }
- my $invoice_from =
- scalar(@_)
- ? shift
- : ( $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum )
- );
+ 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*$/;
+ }
- my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+ 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($template, $invoice_from)
- $self->email($template, $invoice_from)
+ #$self->email_invoice(\%opt)
+ $self->email(\%opt)
if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
- #$self->print_invoice($template)
- $self->print($template)
+ #$self->print_invoice(\%opt)
+ $self->print(\%opt)
if grep { $_ eq 'POST' } @invoicing_list; #postal
- $self->fax_invoice($template)
+ $self->fax_invoice(\%opt)
if grep { $_ eq 'FAX' } @invoicing_list; #fax
'';
}
-=item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
+=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
Emails this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to
+two values for templatename and invoice_from.
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+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
my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
or die "invalid invoice number: " . $opt{invnum};
- my @args = ( $opt{template} );
- push @args, $opt{invoice_from}
- if exists($opt{invoice_from}) && $opt{invoice_from};
+ my %args = ( 'template' => $opt{template} );
+ $args{$_} = $opt{$_}
+ foreach grep { exists($opt{$_}) && $opt{$_} }
+ qw( invoice_from notice_name no_coupon );
- my $error = $self->email( @args );
+ my $error = $self->email( \%args );
die $error if $error;
}
#sub email_invoice {
sub email {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
- my $invoice_from =
- scalar(@_)
- ? shift
- : ( $self->_agent_invoice_from || #XXX should go away
- $conf->config('invoice_from', $self->cust_main->agentnum )
- );
+ my( $template, $invoice_from, $notice_name, $no_coupon );
+ if ( ref($_[0]) ) {
+ my $opt = shift;
+ $template = $opt->{'template'} || '';
+ $invoice_from = $opt->{'invoice_from'};
+ $notice_name = $opt->{'notice_name'} || 'Invoice';
+ $no_coupon = $opt->{'no_coupon'} || 0;
+ } else {
+ $template = scalar(@_) ? shift : '';
+ $invoice_from = shift if scalar(@_);
+ $notice_name = 'Invoice';
+ $no_coupon = 0;
+ }
+
+ $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
+ $conf->config('invoice_from', $self->cust_main->agentnum );
my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
$self->cust_main->invoicing_list;
- #better to notify this person than silence
- @invoicing_list = ($invoice_from) unless @invoicing_list;
+ if ( ! @invoicing_list ) { #no recipients
+ if ( $conf->exists('cust_bill-no_recipients-error') ) {
+ die 'No recipients for customer #'. $self->custnum;
+ } else {
+ #default: better to notify this person than silence
+ @invoicing_list = ($invoice_from);
+ }
+ }
my $subject = $self->email_subject($template);
my $error = send_email(
$self->generate_email(
- 'from' => $invoice_from,
- 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
- 'subject' => $subject,
- 'template' => $template,
+ 'from' => $invoice_from,
+ 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+ 'subject' => $subject,
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ 'no_coupon' => $no_coupon,
)
);
die "can't email invoice: $error\n" if $error;
eval qq("$subject");
}
-=item lpr_data [ TEMPLATENAME ]
+=item lpr_data HASHREF | [ TEMPLATE ]
Returns the postscript or plaintext for this invoice as an arrayref.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional value
+for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
=cut
sub lpr_data {
- my( $self, $template) = @_;
- $conf->exists('invoice_latex')
- ? [ $self->print_ps('', $template) ]
- : [ $self->print_text('', $template) ];
+ my $self = shift;
+ my( $template, $notice_name );
+ if ( ref($_[0]) ) {
+ my $opt = shift;
+ $template = $opt->{'template'} || '';
+ $notice_name = $opt->{'notice_name'} || 'Invoice';
+ } else {
+ $template = scalar(@_) ? shift : '';
+ $notice_name = 'Invoice';
+ }
+
+ my %opt = (
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ );
+
+ my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
+ [ $self->$method( \%opt ) ];
}
-=item print [ TEMPLATENAME ]
+=item print HASHREF | [ TEMPLATE ]
Prints this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
=cut
#sub print_invoice {
sub print {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
+ my( $template, $notice_name );
+ if ( ref($_[0]) ) {
+ my $opt = shift;
+ $template = $opt->{'template'} || '';
+ $notice_name = $opt->{'notice_name'} || 'Invoice';
+ } else {
+ $template = scalar(@_) ? shift : '';
+ $notice_name = 'Invoice';
+ }
+
+ my %opt = (
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ );
- do_print $self->lpr_data($template);
+ if($conf->exists('invoice_print_pdf')) {
+ # Add the invoice to the current batch.
+ $self->batch_invoice(\%opt);
+ }
+ else {
+ do_print $self->lpr_data(\%opt);
+ }
}
-=item fax_invoice [ TEMPLATENAME ]
+=item fax_invoice HASHREF | [ TEMPLATE ]
Faxes this invoice.
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
=cut
sub fax_invoice {
my $self = shift;
- my $template = scalar(@_) ? shift : '';
+ my( $template, $notice_name );
+ if ( ref($_[0]) ) {
+ my $opt = shift;
+ $template = $opt->{'template'} || '';
+ $notice_name = $opt->{'notice_name'} || 'Invoice';
+ } else {
+ $template = scalar(@_) ? shift : '';
+ $notice_name = 'Invoice';
+ }
die 'FAX invoice destination not (yet?) supported with plain text invoices.'
unless $conf->exists('invoice_latex');
my $dialstring = $self->cust_main->getfield('fax');
#Check $dialstring?
- my $error = send_fax( 'docdata' => $self->lpr_data($template),
+ my %opt = (
+ 'template' => $template,
+ 'notice_name' => $notice_name,
+ );
+
+ my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
'dialstring' => $dialstring,
);
die $error if $error;
}
+=item batch_invoice [ HASHREF ]
+
+Place this invoice into the open batch (see C<FS::bill_batch>). If there
+isn't an open batch, one will be created.
+
+=cut
+
+sub batch_invoice {
+ my ($self, $opt) = @_;
+ my $batch = FS::bill_batch->get_open_batch;
+ my $cust_bill_batch = FS::cust_bill_batch->new({
+ batchnum => $batch->batchnum,
+ invnum => $self->invnum,
+ });
+ return $cust_bill_batch->insert($opt);
+}
+
=item ftp_invoice [ TEMPLATENAME ]
Sends this invoice data via FTP.
}
sub realtime_bop {
- my( $self, $method ) = @_;
+ my( $self, $method ) = (shift,shift);
+ my %opt = @_;
my $cust_main = $self->cust_main;
my $balance = $cust_main->balance;
$cust_main->realtime_bop($method, $amount,
'description' => $description,
'invnum' => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+# 'apply' => 1,
+ 'apply_to_invoice' => 1,
+ %opt,
+ #what we want:
+ #this changes application behavior: auto payments
+ #triggered against a specific invoice are now applied
+ #to that invoice instead of oldest open.
+ #seem okay to me...
);
}
$self->cust_main->agent_invoice_from;
}
-=item print_text [ TIME [ , TEMPLATE ] ]
+=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
Returns an text invoice, as a list of lines.
-TIME an optional value used to control the printing of overdue messages. The
+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_text {
- my( $self, $today, $template, %opt ) = @_;
+ 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{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+ $params{$_} = $opt{$_}
+ foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
$self->print_generic( %params );
}
-=item print_latex [ TIME [ , TEMPLATE ] ]
+=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
See print_ps and print_pdf for methods that return PostScript and PDF output.
-TIME an optional value used to control the printing of overdue messages. The
+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, $today, $template, %opt ) = @_;
+ 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{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+ $params{$_} = $opt{$_}
+ foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
$template ||= $self->_agent_template;
close $lh;
$params{'logo_file'} = $lh->filename;
+ if($conf->exists('invoice-barcode')){
+ my $png_file = $self->invoice_barcode($dir);
+ my $eps_file = $png_file;
+ $eps_file =~ s/\.png$/.eps/g;
+ $png_file =~ /(barcode.*png)/;
+ $png_file = $1;
+ $eps_file =~ /(barcode.*eps)/;
+ $eps_file = $1;
+
+ my $curr_dir = cwd();
+ chdir($dir);
+ # after painfuly long experimentation, it was determined that sam2p won't
+ # accept : and other chars in the path, no matter how hard I tried to
+ # escape them, hence the chdir (and chdir back, just to be safe)
+ system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
+ or die "sam2p failed: $!\n";
+ unlink($png_file);
+ chdir($curr_dir);
+
+ $params{'barcode_file'} = $eps_file;
+ }
+
my @filled_in = $self->print_generic( %params );
my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
close $fh;
$fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
- return ($1, $params{'logo_file'});
+ return ($1, $params{'logo_file'}, $params{'barcode_file'});
+
+}
+=item invoice_barcode DIR_OR_FALSE
+
+Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
+it is taken as the temp directory where the PNG file will be generated and the
+PNG file name is returned. Otherwise, the PNG image itself is returned.
+
+=cut
+
+sub invoice_barcode {
+ my ($self, $dir) = (shift,shift);
+
+ my $gdbar = new GD::Barcode('Code39',$self->invnum);
+ die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
+ my $gd = $gdbar->plot(Height => 30);
+
+ if($dir) {
+ my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
+ DIR => $dir,
+ SUFFIX => '.png',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ print $bh $gd->png or die "cannot write barcode to file: $!\n";
+ my $png_file = $bh->filename;
+ close $bh;
+ return $png_file;
+ }
+ return $gd->png;
}
-=item print_generic OPTIONS_HASH
+=item print_generic OPTION => VALUE ...
Internal method - returns a filled-in template for this invoice as a scalar.
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?) problems to change them all to '%.2f' ?
+# (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 "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
+ warn "$me print_generic called on $self with suffix $params{template}\n"
if $DEBUG;
my $format = $params{format};
'template' => [ '{', '}' ],
);
+ warn "$me print_generic creating template\n"
+ if $DEBUG > 1;
+
#create the template
my $template = $params{template} ? $params{template} : $self->_agent_template;
my $templatefile = "invoice_$format";
$templatefile .= "_$template"
- if length($template);
+ if length($template) && $conf->exists($templatefile."_$template");
my @invoice_template = map "$_\n", $conf->config($templatefile)
or die "cannot load config data $templatefile";
@invoice_template = _translate_old_latex_format(@invoice_template);
}
+ warn "$me print_generic creating T:T object\n"
+ if $DEBUG > 1;
+
my $text_template = new Text::Template(
TYPE => 'ARRAY',
SOURCE => \@invoice_template,
DELIMITERS => $delimiters{$format},
);
+ warn "$me print_generic compiling T:T object\n"
+ if $DEBUG > 1;
+
$text_template->compile()
or die "Can't compile $templatefile: $Text::Template::ERROR\n";
'smallfooter' => sub { map "$_", @_ },
'returnaddress' => sub { map "$_", @_ },
'coupon' => sub { map "$_", @_ },
+ 'summary' => sub { map "$_", @_ },
},
'html' => {
'notes' =>
} @_
},
'coupon' => sub { "" },
+ 'summary' => sub { "" },
},
'template' => {
'notes' =>
} @_
},
'coupon' => sub { "" },
+ 'summary' => sub { "" },
},
);
my $nbsp = $nbsps{$format};
my %escape_functions = ( 'latex' => \&_latex_escape,
- 'html' => \&encode_entities,
+ 'html' => \&_html_escape_nbsp,#\&encode_entities,
'template' => sub { shift },
);
my $escape_function = $escape_functions{$format};
+ my $escape_function_nonbsp = ($format eq 'html')
+ ? \&_html_escape : $escape_function;
- my %date_formats = ( 'latex' => '%b %o, %Y',
- 'html' => '%b %o, %Y',
+ my %date_formats = ( 'latex' => $date_format_long,
+ 'html' => $date_format_long,
'template' => '%s',
);
+ $date_formats{'html'} =~ s/ / /g;
+
my $date_format = $date_formats{$format};
my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
);
my $embolden_function = $embolden_functions{$format};
+ my %newline_tokens = ( 'latex' => '\\\\',
+ 'html' => '<br>',
+ 'template' => "\n",
+ );
+ my $newline_token = $newline_tokens{$format};
+
+ warn "$me generating template variables\n"
+ if $DEBUG > 1;
# generate template variables
my $returnaddress;
}
+ warn "$me generating invoice data\n"
+ if $DEBUG > 1;
+
+ my $agentnum = $self->cust_main->agentnum;
+
my %invoice_data = (
- 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
- 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
- 'custnum' => $cust_main->display_custnum,
+
+ #invoice from info
+ 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
+ 'company_address' => join("\n", $conf->config('company_address', $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),
- 'agent' => &$escape_function($cust_main->agent->agent),
- 'agent_custid' => &$escape_function($cust_main->agent_custid),
- 'payname' => &$escape_function($cust_main->payname),
- 'company' => &$escape_function($cust_main->company),
- 'address1' => &$escape_function($cust_main->address1),
- 'address2' => &$escape_function($cust_main->address2),
- 'city' => &$escape_function($cust_main->city),
- 'state' => &$escape_function($cust_main->state),
- 'zip' => &$escape_function($cust_main->zip),
- 'fax' => &$escape_function($cust_main->fax),
- 'returnaddress' => $returnaddress,
- #'quantity' => 1,
+ 'today' => time2str($date_format_long, $today),
'terms' => $self->terms,
'template' => $template, #params{'template'},
- #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
- # better hang on to conf_dir for a while
- 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
- 'page' => 1,
- 'total_pages' => 1,
+ 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
'current_charges' => sprintf("%.2f", $self->charged),
- 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
+ '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'),
+
+ #layout info -- would be fancy to calc some of this and bury the template
+ # here in the code
+ 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
+ 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
+ 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
+ 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
+ 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
+ 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
+ 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
+ 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
+ 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
+ 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+
+ # 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,
+
);
+
+ my $min_sdate = 999999999999;
+ my $max_edate = 0;
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+ $min_sdate = $cust_bill_pkg->sdate
+ if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
+ $max_edate = $cust_bill_pkg->edate
+ if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
+ }
- my $countrydefault = $conf->config('countrydefault') || 'US';
- my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+ $invoice_data{'bill_period'} = '';
+ $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
+ . " to " . time2str('%e %h', $max_edate)
+ if ($max_edate != 0 && $min_sdate != 999999999999);
+
+ $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';
+ $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
+
+ 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{'logo_file'} = $params{'logo_file'}
if $params{'logo_file'};
+ $invoice_data{'barcode_file'} = $params{'barcode_file'}
+ if $params{'barcode_file'};
+ $invoice_data{'barcode_img'} = $params{'barcode_img'}
+ if $params{'barcode_img'};
+ $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
+ if $params{'barcode_cid'};
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;
+
+ warn "$me substituting variables in notes, footer, smallfooter\n"
+ if $DEBUG > 1;
- #do variable substitution in notes, footer, smallfooter
- foreach my $include (qw( notes footer smallfooter coupon )) {
+ my @include = (qw( notes footer smallfooter ));
+ push @include, 'coupon' unless $params{'no_coupon'};
+ foreach my $include (@include) {
my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
my @inc_src;
'template' => '',
);
my $other_money_char = $other_money_chars{$format};
+ $invoice_data{'dollar'} = $other_money_char;
my @detail_items = ();
my @total_items = ();
$invoice_data{'total_items'} = \@total_items;
$invoice_data{'buf'} = \@buf;
$invoice_data{'sections'} = \@sections;
-
+
+ warn "$me generating sections\n"
+ if $DEBUG > 1;
+
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
+ '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
+ '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 ) {
- push @sections, $self->_items_sections( $late_sections );
+ ($extra_sections, $extra_lines) =
+ $self->_items_extra_usage_sections($escape_function_nonbsp, $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_nonbsp,
+ $extra_sections,
+ $format, #bah
+ );
+ if ($conf->exists('svc_phone_sections')) {
+ my ($phone_sections, $phone_lines) =
+ $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
+ push @{$late_sections}, @$phone_sections;
+ push @detail_items, @$phone_lines;
+ }
}else{
push @sections, { 'description' => '', 'subtotal' => '' };
}
)
{
+ warn "$me adding previous balances\n"
+ if $DEBUG > 1;
+
foreach my $line_item ( $self->_items_previous ) {
my $detail = {
$money_char. sprintf("%10.2f", $pr_total) ];
push @buf, ['',''];
}
+
+ if ( $conf->exists('svc_phone-did-summary') ) {
+ warn "$me adding DID summary\n"
+ if $DEBUG > 1;
+
+ my ($didsummary,$minutes) = $self->_did_summary;
+ my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
+ push @detail_items,
+ { 'description' => $didsummary_desc,
+ 'ext_description' => [ $didsummary, $minutes ],
+ }
+ if !$multisection;
+ }
foreach my $section (@sections, @$late_sections) {
+ warn "$me adding section \n". Dumper($section)
+ if $DEBUG > 1;
+
+ # begin some normalization
+ $section->{'subtotal'} = $section->{'amount'}
+ if $multisection
+ && !exists($section->{subtotal})
+ && exists($section->{amount});
+
+ $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;
+ # continue some normalization
+ $section->{'amount'} = $section->{'subtotal'}
+ if $multisection;
+
+
if ( $section->{'description'} ) {
push @buf, ( [ &$escape_function($section->{'description'}), '' ],
[ '', '' ],
);
}
+ warn "$me setting options\n"
+ if $DEBUG > 1;
+
+ 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;
+ $options{'multisection'} = $multisection;
+
+ warn "$me searching for line items\n"
+ if $DEBUG > 1;
foreach my $line_item ( $self->_items_pkg(%options) ) {
+
+ warn "$me adding line item $line_item\n"
+ if $DEBUG > 1;
+
my $detail = {
ext_description => [],
};
}
- if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+ $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;
}
+ warn "$me adding taxes\n"
+ if $DEBUG > 1;
+
foreach my $tax ( $self->_items_tax ) {
$taxtotal += $tax->{'amount'};
}
}
$invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
-
+
push @buf,['','-----------'];
push @buf,[( $conf->exists('disable_previous_balance')
? 'Total Charges'
{
my $total = {};
- $total->{'total_item'} = &$embolden_function('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',
- $self->charged + ( $conf->exists('disable_previous_balance')
- ? 0
- : $pr_total
- )
- )
- );
+ &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
if ( $multisection ) {
- $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
- sprintf('%.2f', $self->charged );
+ 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,['Total Charges',
+ push @buf,[$item,
$money_char.
- sprintf( '%10.2f', $self->charged +
- ( $conf->exists('disable_previous_balance')
- ? 0
- : $pr_total
- )
- )
+ sprintf( '%10.2f', $amount )
];
push @buf,['',''];
}
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 ) {
if ( $multisection ) {
$adjust_section->{'subtotal'} = $other_money_char.
sprintf('%.2f', $adjusttotal);
- push @sections, $adjust_section;
+ push @sections, $adjust_section
+ unless $adjust_section->{sort_weight};
}
{
$total->{'total_item'} = &$embolden_function($self->balance_due_msg);
$total->{'total_amount'} =
&$embolden_function(
- $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
+ $other_money_char. sprintf('%.2f', $summarypage
+ ? $self->charged +
+ $self->billing_balance
+ : $self->owed + $pr_total
+ )
);
- if ( $multisection ) {
+ if ( $multisection && !$adjust_section->{sort_weight} ) {
$adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
$total->{'total_amount'};
}else{
push @buf,[$self->balance_due_msg, $money_char.
sprintf("%10.2f", $balance_due ) ];
}
+
+ if ( $conf->exists('previous_balance-show_credit')
+ and $cust_main->balance < 0 ) {
+ my $credit_total = {
+ 'total_item' => &$embolden_function($self->credit_balance_msg),
+ 'total_amount' => &$embolden_function(
+ $other_money_char. sprintf('%.2f', -$cust_main->balance)
+ ),
+ };
+ if ( $multisection ) {
+ $adjust_section->{'posttotal'} .= $newline_token .
+ $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+ }
+ else {
+ push @total_items, $credit_total;
+ }
+ push @buf,['','-----------'];
+ push @buf,[$self->credit_balance_msg, $money_char.
+ sprintf("%10.2f", -$cust_main->balance ) ];
+ }
}
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
}
}
-=item print_ps [ TIME [ , TEMPLATE ] ]
+# 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.
-TIME an optional value used to control the printing of overdue messages. The
+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 ($file, $logofile, $barcodefile) = $self->print_latex(@_);
my $ps = generate_ps($file);
- unlink($lfile);
+ unlink($logofile);
+ unlink($barcodefile);
$ps;
}
-=item print_pdf [ TIME [ , TEMPLATE ] ]
+=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
Returns an PDF invoice, as a scalar.
-TIME an optional value used to control the printing of overdue messages. The
+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 ($file, $logofile, $barcodefile) = $self->print_latex(@_);
my $pdf = generate_pdf($file);
- unlink($lfile);
+ unlink($logofile);
+ unlink($barcodefile);
$pdf;
}
-=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
+=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
Returns an HTML invoice, as a scalar.
-TIME an optional value used to control the printing of overdue messages. The
+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.
-CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
+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] ) {
+ if ( ref($_[0]) ) {
%params = %{ shift() };
}else{
$params{'time'} = shift;
}
$params{'format'} = 'html';
-
+
$self->print_generic( %params );
}
$value;
}
+sub _html_escape {
+ my $value = shift;
+ encode_entities($value);
+ $value;
+}
+
+sub _html_escape_nbsp {
+ my $value = _html_escape(shift);
+ $value =~ s/ +/ /g;
+ $value;
+}
+
#utility methods for print_*
sub _translate_old_latex_format {
$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, '[@--',
sub terms {
my $self = shift;
- #check for an invoice- specific override (eventually)
+ #check for an invoice-specific override
+ return $self->invoice_terms if $self->invoice_terms;
#check for a customer- specific override
- return $self->cust_main->invoice_terms
- if $self->cust_main->invoice_terms;
+ my $cust_main = $self->cust_main;
+ return $cust_main->invoice_terms if $cust_main->invoice_terms;
#use configured default
$conf->config('invoice_default_terms') || '';
my $msg = 'Balance Due';
return $msg unless $self->terms;
if ( $self->due_date ) {
- $msg .= ' - Please pay by '. $self->due_date2str('%x');
+ $msg .= ' - Please pay by '. $self->due_date2str($date_format);
} elsif ( $self->terms ) {
$msg .= ' - '. $self->terms;
}
my $duedate = '';
if ( $conf->exists('invoice_default_terms')
&& $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+ $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
}
$duedate;
}
+sub credit_balance_msg { 'Credit Balance Remaining' }
+
=item invnum_date_pretty
Returns a string with the invoice number and date, for example:
sub _date_pretty {
my $self = shift;
- time2str('%x', $self->_date);
+ 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 %s = ();
- my %l = ();
+ my %subtotal = ();
+ my %late_subtotal = ();
+ my %not_tax = ();
foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
{
- if ( $cust_bill_pkg->pkgnum > 0 ) {
my $usage = $cust_bill_pkg->usage;
foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
- my $desc = $display->section;
- my $type = $display->type;
+ 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 ) {
+ if ( $display->post_total && !$summarypage ) {
if (! $type || $type eq 'S') {
- $l{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0;
}
if (! $type) {
- $l{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'R') {
- $l{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
+ $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'U') {
- $l{$desc} += $usage;
+ $late_subtotal{$section} += $usage
+ unless scalar(@$extra_sections);
}
} else {
+
+ next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
if (! $type || $type eq 'S') {
- $s{$desc} += $cust_bill_pkg->setup
- if ( $cust_bill_pkg->setup != 0 );
+ $subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0;
}
if (! $type) {
- $s{$desc} += $cust_bill_pkg->recur
- if ( $cust_bill_pkg->recur != 0 );
+ $subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'R') {
- $s{$desc} += $cust_bill_pkg->recur - $usage
- if ( $cust_bill_pkg->recur != 0 );
+ $subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0;
}
if ($type && $type eq 'U') {
- $s{$desc} += $usage;
+ $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', {});
+ push @sections, '' if exists($subtotal{''});
+ } 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 { my($href, %opt) = @_;
+ ($opt{dollar} || ''). $href->{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);
+
+ my $money_char = '$';
+ 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}}}";
+ };
+ $money_char = '\\dollar';
+ }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>!;
+ };
+ #$money_char = $conf->config('money_char') || '$';
+ $money_char = ''; # this is madness
+ }
+
+ sub {
+ #my @args = @_;
+ my $href = shift;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ my $dollar = '';
+ $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+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>!;
+ };
}
- push @$late, map { { 'description' => $_,
- 'subtotal' => $l{$_},
- 'post_total' => 1,
- } } sort keys %l;
- map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
+ 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 total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class. FORMAT is either html or latex
+
+=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;
+ };
+
+}
+
+#sub _items_extra_usage_sections {
+# my $self = shift;
+# my $escape = shift;
+#
+# my %sections = ();
+#
+# my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
+# foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+# {
+# next unless $cust_bill_pkg->pkgnum > 0;
+#
+# foreach my $section ( keys %usage_class ) {
+#
+# my $usage = $cust_bill_pkg->usage($section);
+#
+# next unless $usage && $usage > 0;
+#
+# $sections{$section} ||= 0;
+# $sections{$section} += $usage;
+#
+# }
+#
+# }
+#
+# map { { 'description' => &{$escape}($_),
+# 'subtotal' => $sections{$_},
+# 'summarized' => '',
+# 'tax_section' => '',
+# }
+# }
+# sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
+
+sub _items_extra_usage_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ foreach my $classnum ( keys %usage_class ) {
+ my $section = $usage_class{$classnum}->classname;
+ $classnums{$section} = $classnum;
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+ $sections{$section}{amount} += $amount; #subtotal
+ $sections{$section}{calls}++;
+ $sections{$section}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($desc) > 50;
+
+ $lines{$section}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$section}{$desc}{amount} += $amount;
+ $lines{$section}{$desc}{calls}++;
+ $lines{$section}{$desc}{duration} += $detail->duration;
+
+ }
+ }
+ }
+
+ my %sectionmap = ();
+ foreach (keys %sections) {
+ my $usage_class = $usage_class{$classnums{$_}};
+ $sectionmap{$_} = { 'description' => &{$escape}($_),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'sort_weight' => $usage_class->weight,
+ ( $usage_class->format
+ ? ( map { $_ => $usage_class->$_($format) }
+ qw( description_generator header_generator total_generator total_line_generator )
+ )
+ : ()
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ return(\@sections, \@lines);
+
+}
+
+sub _did_summary {
+ my $self = shift;
+ my $end = $self->_date;
+ my $start = $end - 2592000; # 30 days
+ 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);
+ my $phone_deleted;
+ $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
+
+# DID either activated or ported in; cannot be both for same DID simultaneously
+ if ($inserted >= $start && $inserted <= $end && $phone_inserted
+ && (!$phone_inserted->lnp_status
+ || $phone_inserted->lnp_status eq ''
+ || $phone_inserted->lnp_status eq 'native')) {
+ $num_activated++;
+ }
+ else { # this one not so clean, should probably move to (h_)svc_phone
+ my $phone_portedin = qsearchs( 'h_svc_phone',
+ { 'svcnum' => $h_cust_svc->svcnum,
+ 'lnp_status' => 'portedin' },
+ FS::h_svc_phone->sql_h_searchs($end),
+ );
+ $num_portedin++ if $phone_portedin;
+ }
+
+# DID either deactivated or ported out; cannot be both for same DID simultaneously
+ if($deleted >= $start && $deleted <= $end && $phone_deleted
+ && (!$phone_deleted->lnp_status
+ || $phone_deleted->lnp_status ne 'portingout')) {
+ $num_deactivated++;
+ }
+ elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+ && $phone_deleted->lnp_status
+ && $phone_deleted->lnp_status eq 'portingout') {
+ $num_portedout++;
+ }
+
+ # increment usage minutes
+ my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
+ foreach my $cdr ( @cdrs ) {
+ $minutes += $cdr->billsec/60;
+ }
+
+ # 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_svc_phone_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ my @header = $cust_bill_pkg->details_header;
+ next unless scalar(@header);
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+ my $phonenum = $detail->phonenum;
+ next unless $phonenum;
+
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$phonenum} ||= { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => -1,
+ 'phonenum' => $phonenum,
+ };
+ $sections{$phonenum}{amount} += $amount; #subtotal
+ $sections{$phonenum}{calls}++;
+ $sections{$phonenum}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, 50). '...'
+ if $format eq 'latex' && length($desc) > 50;
+
+ $lines{$phonenum}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$phonenum}{$desc}{amount} += $amount;
+ $lines{$phonenum}{$desc}{calls}++;
+ $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+ my $line = $usage_class{$detail->classnum}->classname;
+ $sections{"$phonenum $line"} ||=
+ { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => $usage_class{$detail->classnum}->weight,
+ 'phonenum' => $phonenum,
+ 'header' => [ @header ],
+ };
+ $sections{"$phonenum $line"}{amount} += $amount; #subtotal
+ $sections{"$phonenum $line"}{calls}++;
+ $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+ $lines{"$phonenum $line"}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+ $lines{"$phonenum $line"}{$desc}{calls}++;
+ $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+ push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+ $detail->formatted('format' => $format);
+
+ }
+ }
+
+ my %sectionmap = ();
+ my $simple = new FS::usage_class { format => 'simple' }; #bleh
+ foreach ( keys %sections ) {
+ my @header = @{ $sections{$_}{header} || [] };
+ my $usage_simple =
+ new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
+ my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+ my $usage_class = $summary ? $simple : $usage_simple;
+ my $ending = $summary ? ' usage charges' : '';
+ my %gen_opt = ();
+ unless ($summary) {
+ $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+ }
+ $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'phonenum' => $sections{$_}{phonenum},
+ 'sort_weight' => $sections{$_}{sort_weight},
+ 'post_total' => $summary, #inspire pagebreak
+ (
+ ( map { $_ => $usage_class->$_($format, %gen_opt) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ )
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+ $a->{sort_weight} <=> $b->{sort_weight}
+ }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ 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);
}
my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
my @b = ();
foreach ( @pr_cust_bill ) {
+ my $date = $conf->exists('invoice_show_prior_due_date')
+ ? 'due '. $_->due_date2str($date_format)
+ : time2str($date_format, $_->_date);
push @b, {
- 'description' => 'Previous Balance, Invoice #'. $_->invnum.
- ' ('. time2str('%x',$_->_date). ')',
+ 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
#'pkgpart' => 'N/A',
'pkgnum' => 'N/A',
'amount' => sprintf("%.2f", $_->owed),
sub _items_pkg {
my $self = shift;
+ my %options = @_;
+
+ warn "$me _items_pkg searching for all package line items\n"
+ if $DEBUG > 1;
+
my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+ warn "$me _items_pkg filtering line items\n"
+ if $DEBUG > 1;
+ my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+ if ($options{section} && $options{section}->{condensed}) {
+
+ warn "$me _items_pkg condensing section\n"
+ if $DEBUG > 1;
+
+ 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;
+ }
+
+ warn "$me _items_pkg returning ". scalar(@items). " items\n"
+ if $DEBUG > 1;
+
+ @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;
+ return 0 unless $a->itemdesc cmp $b->itemdesc;
+ return -1 if $b->itemdesc eq 'Tax';
+ return 1 if $a->itemdesc eq 'Tax';
+ return -1 if $b->itemdesc eq 'Other surcharges';
+ return 1 if $a->itemdesc eq 'Other surcharges';
+ $a->itemdesc cmp $b->itemdesc;
}
sub _items_tax {
sub _items_cust_bill_pkg {
my $self = shift;
- my $cust_bill_pkg = shift;
+ my $cust_bill_pkgs = shift;
my %opt = @_;
my $format = $opt{format} || '';
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 $multisection = $opt{multisection} || '';
+ my $discount_show_always = 0;
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
- foreach my $cust_bill_pkg ( @$cust_bill_pkg )
+ foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
{
- foreach ( $s, $r, $u ) {
+ warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
+ if $DEBUG > 1;
+
+ $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
+ && $conf->exists('discount-show-always'));
+
+ 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, { %$_ };
+ push @b, { %$_ }
+ unless ( $_->{amount} == 0 && !$discount_show_always );
$_ = undef;
}
}
? $_->section eq $section
: 1
}
+ #grep { !$_->summary || !$summary_page } # bunk!
+ grep { !$_->summary || $multisection }
$cust_bill_pkg->cust_bill_pkg_display
)
{
- my $type = $display->type;
+ warn "$me _items_cust_bill_pkg considering display item $display\n"
+ if $DEBUG > 1;
- my $cust_pkg = $cust_bill_pkg->cust_pkg;
+ my $type = $display->type;
my $desc = $cust_bill_pkg->desc;
$desc = substr($desc, 0, 50). '...'
if ( $cust_bill_pkg->pkgnum > 0 ) {
+ warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
+ if $DEBUG > 1;
+
+ my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
+ warn "$me _items_cust_bill_pkg adding setup\n"
+ if $DEBUG > 1;
+
my $description = $desc;
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
my @d = ();
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date)
- unless $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->hidden;
+ unless ( $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->hidden )
+ {
+
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short($self->_date, undef, 'I')
+ unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($loc, 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->recur != 0 &&
+ if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
+ ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
( !$type || $type eq 'R' || $type eq 'U' )
)
{
+ warn "$me _items_cust_bill_pkg adding recur/usage\n"
+ if $DEBUG > 1;
+
my $is_summary = $display->summary;
- my $description = $is_summary ? "Usage charges" : $desc;
+ my $description = ($is_summary && $type && $type eq 'U')
+ ? "Usage charges" : $desc;
- unless ( $conf->exists('disable_line_item_date_ranges') ) {
- $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
- " - ". time2str("%x", $cust_bill_pkg->edate). ")";
- }
+ $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
+ " - ". time2str($date_format, $cust_bill_pkg->edate).
+ ")"
+ unless $conf->exists('disable_line_item_date_ranges');
my @d = ();
my @dates = ( $self->_date );
my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
push @dates, $prev->sdate if $prev;
+ push @dates, undef if !$prev;
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short(@dates)
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate)
- unless $cust_pkg->part_pkg->hide_svc_detail
+ unless ( $cust_pkg->part_pkg->hide_svc_detail
|| $cust_bill_pkg->itemdesc
|| $cust_bill_pkg->hidden
- || $is_summary;
+ || $is_summary && $type && $type eq 'U' )
+ {
+
+ warn "$me _items_cust_bill_pkg adding service details\n"
+ if $DEBUG > 1;
+
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short(@dates, 'I')
+ #$cust_bill_pkg->edate,
+ #$cust_bill_pkg->sdate)
+ unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+ warn "$me _items_cust_bill_pkg done adding service details\n"
+ if $DEBUG > 1;
+
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($loc, 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');
+ }
+
+ unless ( $is_summary ) {
+ warn "$me _items_cust_bill_pkg adding details\n"
+ if $DEBUG > 1;
+
+ #instead of omitting details entirely in this case (unwanted side
+ # effects), just omit CDRs
+ $details_opt{'format_function'} = sub { () }
+ if $type && $type eq 'R';
+
+ push @d, $cust_bill_pkg->details(%details_opt);
+ }
+
+ warn "$me _items_cust_bill_pkg calculating amount\n"
+ if $DEBUG > 1;
my $amount = 0;
if (!$type) {
$amount = $cust_bill_pkg->recur;
- }elsif($type eq 'R') {
+ } elsif ($type eq 'R') {
$amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
- }elsif($type eq 'U') {
+ } elsif ($type eq 'U') {
$amount = $cust_bill_pkg->usage;
}
if ( !$type || $type eq 'R' ) {
+ warn "$me _items_cust_bill_pkg adding recur\n"
+ if $DEBUG > 1;
+
if ( $cust_bill_pkg->hidden ) {
$r->{amount} += $amount;
$r->{unit_amount} += $cust_bill_pkg->unitrecur;
};
}
- } elsif ( $amount ) { # && $type eq 'U'
+ } else { # $type eq 'U'
+
+ warn "$me _items_cust_bill_pkg adding usage\n"
+ if $DEBUG > 1;
if ( $cust_bill_pkg->hidden ) {
$u->{amount} += $amount;
} else { #pkgnum tax or one-shot line item (??)
+ warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
+ if $DEBUG > 1;
+
if ( $cust_bill_pkg->setup != 0 ) {
push @b, {
'description' => $desc,
if ( $cust_bill_pkg->recur != 0 ) {
push @b, {
'description' => "$desc (".
- time2str("%x", $cust_bill_pkg->sdate). ' - '.
- time2str("%x", $cust_bill_pkg->edate). ')',
+ time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+ time2str($date_format, $cust_bill_pkg->edate). ')',
'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
};
}
}
- foreach ( $s, $r, $u ) {
- if ( $_ ) {
+ warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
+ if $DEBUG > 1;
+
+ 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, { %$_ };
+ push @b, { %$_ }
+ unless ( $_->{amount} == 0 && !$discount_show_always );
}
}
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
'description' => 'Credit applied '.
- time2str("%x",$_->cust_credit->_date). $reason,
+ time2str($date_format,$_->cust_credit->_date). $reason,
'amount' => sprintf("%.2f",$_->amount),
};
}
push @b, {
'description' => "Payment received ".
- time2str("%x",$_->cust_pay->_date ),
+ time2str($date_format,$_->cust_pay->_date ),
'amount' => sprintf("%.2f", $_->amount )
};
}
}
-=item call_details
+=item call_details [ OPTION => VALUE ... ]
Returns an array of CSV strings representing the call details for this invoice
+The only option available is the boolean prepend_billed_number
=cut
sub call_details {
- my $self = shift;
- map { $_->details( 'format_function' => sub{ shift },
- 'escape_function' => sub{ return() },
- )
- }
- grep { $_->pkgnum }
- $self->cust_bill_pkg;
+ my ($self, %opt) = @_;
+
+ my $format_function = sub { shift };
+
+ if ($opt{prepend_billed_number}) {
+ $format_function = sub {
+ my $detail = shift;
+ my $row = shift;
+
+ $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
+
+ };
+ }
+
+ my @details = map { $_->details( 'format_function' => $format_function,
+ 'escape_function' => sub{ return() },
+ )
+ }
+ grep { $_->pkgnum }
+ $self->cust_bill_pkg;
+ my $header = $details[0];
+ ( $header, grep { $_ ne $header } @details );
}
my $distinct = '';
my $orderby = 'ORDER BY cust_bill._date';
- my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+ my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
=cut
sub owed_sql {
- my $class = shift;
- 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
+ my ($class, $start, $end) = @_;
+ 'charged - '.
+ $class->paid_sql($start, $end). ' - '.
+ $class->credited_sql($start, $end);
}
=item net_sql
=cut
sub net_sql {
- my $class = shift;
- 'charged - '. $class->credited_sql;
+ my ($class, $start, $end) = @_;
+ 'charged - '. $class->credited_sql($start, $end);
}
=item paid_sql
=cut
sub paid_sql {
- #my $class = shift;
+ my ($class, $start, $end) = @_;
+ $start &&= "AND cust_bill_pay._date <= $start";
+ $end &&= "AND cust_bill_pay._date > $end";
+ $start = '' unless defined($start);
+ $end = '' unless defined($end);
"( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
- WHERE cust_bill.invnum = cust_bill_pay.invnum )";
+ WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
}
=item credited_sql
=cut
sub credited_sql {
- #my $class = shift;
+ my ($class, $start, $end) = @_;
+ $start &&= "AND cust_credit_bill._date <= $start";
+ $end &&= "AND cust_credit_bill._date > $end";
+ $start = '' unless defined($start);
+ $end = '' unless defined($end);
"( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
- WHERE cust_bill.invnum = cust_credit_bill.invnum )";
+ WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
+}
+
+=item due_date_sql
+
+Returns an SQL fragment to retrieve the due date of an invoice.
+Currently only supported on PostgreSQL.
+
+=cut
+
+sub due_date_sql {
+'COALESCE(
+ SUBSTRING(
+ COALESCE(
+ cust_bill.invoice_terms,
+ cust_main.invoice_terms,
+ \''.($conf->config('invoice_default_terms') || '').'\'
+ ), E\'Net (\\\\d+)\'
+ )::INTEGER, 0
+) * 86400 + cust_bill._date'
}
-=item search_sql HASHREF
+=item search_sql_where HASHREF
Class method which returns an SQL WHERE fragment to search for parameters
specified in HASHREF. Valid parameters are
=over 4
-=item begin
-
-Epoch date (UNIX timestamp) setting a lower bound for _date values
+=item _date
-=item end
-
-Epoch date (UNIX timestamp) setting an upper bound for _date values
+List reference of start date, end date, as UNIX timestamps.
=item invnum_min
=item agentnum
+=item charged
+
+List reference of charged limits (exclusive).
+
=item owed
+List reference of charged limits (exclusive).
+
+=item open
+
+flag, return open invoices only
+
=item net
+flag, return net invoices only
+
=item days
=item newest_percust
=cut
-sub search_sql {
+sub search_sql_where {
my($class, $param) = @_;
if ( $DEBUG ) {
- warn "$me search_sql called with params: \n".
+ warn "$me search_sql_where called with params: \n".
join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
}
my @search = ();
- if ( $param->{'begin'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date >= $1";
+ #agentnum
+ if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_main.agentnum = $1";
}
- if ( $param->{'end'} =~ /^(\d+)$/ ) {
- push @search, "cust_bill._date < $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";
}
- if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
- push @search, "cust_main.agentnum = $1";
+
+ #charged
+ if ( $param->{charged} ) {
+ my @charged = ref($param->{charged})
+ ? @{ $param->{charged} }
+ : ($param->{charged});
+
+ push @search, map { s/^charged/cust_bill.charged/; $_; }
+ @charged;
}
- push @search, '0 != '. FS::cust_bill->owed_sql
- if $param->{'open'};
+ my $owed_sql = FS::cust_bill->owed_sql;
+
+ #owed
+ if ( $param->{owed} ) {
+ my @owed = ref($param->{owed})
+ ? @{ $param->{owed} }
+ : ($param->{owed});
+ push @search, map { s/^owed/$owed_sql/; $_; }
+ @owed;
+ }
+ #open/net flags
+ push @search, "0 != $owed_sql"
+ if $param->{'open'};
push @search, '0 != '. FS::cust_bill->net_sql
if $param->{'net'};
+ #days
push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
if $param->{'days'};
+ #newest_percust
if ( $param->{'newest_percust'} ) {
#$distinct = 'DISTINCT ON ( cust_bill.custnum )';
}
+ #agent virtualization
my $curuser = $FS::CurrentUser::CurrentUser;
if ( $curuser->username eq 'fs_queue'
&& $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
}
}
-
push @search, $curuser->agentnums_sql;
join(' AND ', @search );