package FS::cust_bill;
use strict;
-use vars qw( @ISA $DEBUG $me $conf
+use vars qw( @ISA $DEBUG $me
$money_char $date_format $rdate_format $date_format_long );
+ # but NOT $conf
use vars qw( $invoice_lines @buf ); #yuck
use Fcntl qw(:flock); #for spool_csv
use Cwd;
use FS::cust_bill_batch;
use FS::cust_bill_pay_pkg;
use FS::cust_credit_bill_pkg;
+use FS::L10N;
@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;
+ my $conf = new FS::Conf; #global
$money_char = $conf->config('money_char') || '$';
$date_format = $conf->config('date_format') || '%x'; #/YY
$rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
sub display_invnum {
my $self = shift;
+ my $conf = $self->conf;
if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
return $self->agent_invid;
} else {
sub apply_payments_and_credits {
my( $self, %options ) = @_;
+ my $conf = $self->conf;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my $self = shift;
my %args = @_;
+ my $conf = $self->conf;
my $me = '[FS::cust_bill::generate_email]';
my $alternative = build MIME::Entity
'Type' => 'multipart/alternative',
- 'Encoding' => '7bit',
+ #'Encoding' => '7bit',
'Disposition' => 'inline'
;
$alternative->attach(
'Type' => 'text/plain',
- #'Encoding' => 'quoted-printable',
- 'Encoding' => '7bit',
+ 'Encoding' => 'quoted-printable',
+ #'Encoding' => '7bit',
'Data' => $data,
'Disposition' => 'inline',
);
sub send {
my $self = shift;
+ my $conf = $self->conf;
my( $template, $invoice_from, $notice_name );
my $agentnums = '';
#sub email_invoice {
sub email {
my $self = shift;
+ my $conf = $self->conf;
my( $template, $invoice_from, $notice_name, $no_coupon );
if ( ref($_[0]) ) {
sub email_subject {
my $self = shift;
+ my $conf = $self->conf;
#my $template = scalar(@_) ? shift : '';
#per-template?
sub lpr_data {
my $self = shift;
+ my $conf = $self->conf;
my( $template, $notice_name );
if ( ref($_[0]) ) {
my $opt = shift;
#sub print_invoice {
sub print {
my $self = shift;
+ my $conf = $self->conf;
my( $template, $notice_name );
if ( ref($_[0]) ) {
my $opt = shift;
sub fax_invoice {
my $self = shift;
+ my $conf = $self->conf;
my( $template, $notice_name );
if ( ref($_[0]) ) {
my $opt = shift;
sub batch_invoice {
my ($self, $opt) = @_;
- my $batch = FS::bill_batch->get_open_batch;
+ my $bill_batch = $self->get_open_bill_batch;
my $cust_bill_batch = FS::cust_bill_batch->new({
- batchnum => $batch->batchnum,
+ batchnum => $bill_batch->batchnum,
invnum => $self->invnum,
});
return $cust_bill_batch->insert($opt);
}
+=item get_open_batch
+
+Returns the currently open batch as an FS::bill_batch object, creating a new
+one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
+enabled)
+
+=cut
+
+sub get_open_bill_batch {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $hashref = { status => 'O' };
+ $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
+ ? $self->cust_main->agentnum
+ : '';
+ my $batch = qsearchs('bill_batch', $hashref);
+ return $batch if $batch;
+ $batch = FS::bill_batch->new($hashref);
+ my $error = $batch->insert;
+ die $error if $error;
+ return $batch;
+}
+
=item ftp_invoice [ TEMPLATENAME ]
Sends this invoice data via FTP.
sub ftp_invoice {
my $self = shift;
+ my $conf = $self->conf;
my $template = scalar(@_) ? shift : '';
$self->send_csv(
sub spool_invoice {
my $self = shift;
+ my $conf = $self->conf;
my $template = scalar(@_) ? shift : '';
$self->spool_csv(
sub realtime_bop {
my( $self, $method ) = (shift,shift);
+ my $conf = $self->conf;
my %opt = @_;
my $cust_main = $self->cust_main;
$params{'time'} = $today if $today;
$params{'template'} = $template if $template;
$params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
+ foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
$self->print_generic( %params );
}
sub print_latex {
my $self = shift;
+ my $conf = $self->conf;
my( $today, $template, %opt );
if ( ref($_[0]) ) {
%opt = %{ shift() };
$params{'time'} = $today if $today;
$params{'template'} = $template if $template;
$params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
+ foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
$template ||= $self->_agent_template;
SUFFIX => '.tex',
UNLINK => 0,
) or die "can't open temp file: $!\n";
+ binmode($fh, ':utf8'); # language support
print $fh join('', @filled_in );
close $fh;
# (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 $conf = $self->conf;
my $today = $params{today} ? $params{today} : time;
warn "$me print_generic called on $self with suffix $params{template}\n"
if $DEBUG;
'total_pages' => 1,
);
-
+
+ #localization
+ my $lh = FS::L10N->get_handle($cust_main->locale);
+ $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
+
my $min_sdate = 999999999999;
my $max_edate = 0;
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
if ($format eq 'latex');
}
- $invoice_data{'po_line'} =
+ $invoice_data{'po_line'} =
( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
- ? &$escape_function("Purchase Order #". $cust_main->payinfo)
+ ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
: $nbsp;
my %money_chars = ( 'latex' => '',
warn "$me generating sections\n"
if $DEBUG > 1;
- my $previous_section = { 'description' => 'Previous Charges',
+ my $previous_section = { 'description' => $self->mt('Previous Charges'),
'subtotal' => $other_money_char.
sprintf('%.2f', $pr_total),
'summarized' => $summarypage ? 'Y' : '',
if $conf->exists('invoice_include_aging');
my $taxtotal = 0;
- my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
+ my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
'subtotal' => $taxtotal, # adjusted below
'summarized' => $summarypage ? 'Y' : '',
};
my $adjusttotal = 0;
- my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
+ my $adjust_section = { 'description' =>
+ $self->mt('Credits, Payments, and Adjustments'),
'subtotal' => 0, # adjusted below
'summarized' => $summarypage ? 'Y' : '',
};
if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
push @buf, ['','-----------'];
- push @buf, [ 'Total Previous Balance',
+ push @buf, [ $self->mt('Total Previous Balance'),
$money_char. sprintf("%10.2f", $pr_total) ];
push @buf, ['',''];
}
push @detail_items,
{ 'description' => $didsummary_desc,
'ext_description' => [ $didsummary, $minutes ],
- }
- if !$multisection;
+ };
}
foreach my $section (@sections, @$late_sections) {
if ( $taxtotal ) {
my $total = {};
- $total->{'total_item'} = 'Sub-total';
+ $total->{'total_item'} = $self->mt('Sub-total');
$total->{'total_amount'} =
$other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
$invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
push @buf,['','-----------'];
- push @buf,[( $conf->exists('disable_previous_balance')
+ push @buf,[$self->mt(
+ $conf->exists('disable_previous_balance')
? 'Total Charges'
: 'Total New Charges'
),
{
my $total = {};
- my $item = 'Total';
+ my $item = $self->mt('Total');
$item = $conf->config('previous_balance-exclude_from_total')
|| 'Total New Charges'
if $conf->exists('previous_balance-exclude_from_total');
&$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
if ( $multisection ) {
if ( $adjust_section->{'sort_weight'} ) {
- $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
- sprintf("%.2f", ($self->billing_balance || 0) );
+ $adjust_section->{'posttotal'} = $self->mt('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 );
+ $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
+ $other_money_char. sprintf('%.2f', $self->charged );
}
}else{
push @total_items, $total;
}
#setup subroutine for the template
- sub FS::cust_bill::_template::invoice_lines {
- my $lines = shift || scalar(@FS::cust_bill::_template::buf);
+ #sub FS::cust_bill::_template::invoice_lines { # good god, no
+ $invoice_data{invoice_lines} = sub { # much better
+ my $lines = shift || scalar(@buf);
map {
- scalar(@FS::cust_bill::_template::buf)
- ? shift @FS::cust_bill::_template::buf
+ scalar(@buf)
+ ? shift @buf
: [ '', '' ];
}
( 1 .. $lines );
- }
+ };
my $lines;
my @collect;
while (@buf) {
push @collect, split("\n",
- $text_template->fill_in( HASH => \%invoice_data,
- PACKAGE => 'FS::cust_bill::_template'
- )
+ $text_template->fill_in( HASH => \%invoice_data )
);
- $FS::cust_bill::_template::page++;
+ $invoice_data{'page'}++;
}
map "$_\n", @collect;
}else{
sub terms {
my $self = shift;
+ my $conf = $self->conf;
#check for an invoice-specific override
return $self->invoice_terms if $self->invoice_terms;
sub balance_due_msg {
my $self = shift;
- my $msg = 'Balance Due';
+ my $msg = $self->mt('Balance Due');
return $msg unless $self->terms;
if ( $self->due_date ) {
- $msg .= ' - Please pay by '. $self->due_date2str($date_format);
+ $msg .= ' - ' . $self->mt('Please pay by'). ' '.
+ $self->due_date2str($date_format);
} elsif ( $self->terms ) {
$msg .= ' - '. $self->terms;
}
sub balance_due_date {
my $self = shift;
+ my $conf = $self->conf;
my $duedate = '';
if ( $conf->exists('invoice_default_terms')
&& $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
$duedate;
}
-sub credit_balance_msg { 'Credit Balance Remaining' }
+sub credit_balance_msg {
+ my $self = shift;
+ $self->mt('Credit Balance Remaining')
+}
=item invnum_date_pretty
sub invnum_date_pretty {
my $self = shift;
- 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
+ $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
}
=item _date_pretty
sub _items_extra_usage_sections {
my $self = shift;
+ my $conf = $self->conf;
my $escape = shift;
my $format = shift;
my %classnums = ();
my %lines = ();
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
next unless $cust_bill_pkg->pkgnum > 0;
my $desc = $detail->regionname;
my $description = $desc;
- $description = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
$lines{$section}{$desc} ||= {
description => &{$escape}($description),
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_inserted = $h_cust_svc->h_svc_x($inserted+5);
my $phone_deleted;
$phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
}
# increment usage minutes
- my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
- foreach my $cdr ( @cdrs ) {
- $minutes += $cdr->billsec/60;
- }
+ if ( $phone_inserted ) {
+ my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
+ $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
+ }
+ else {
+ warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
+ }
# don't look at this service again
push @seen, $h_cust_svc->svcnum;
quantity => '',
product_code => 'N/A',
section => $section,
- ext_description => [],
+ ext_description => [ $section->{'header'} ],
+ detail_temp => [],
};
$section->{'amount'} += $amount;
$accountcodes{$accountcode}{'amount'} += $amount;
$accountcodes{$accountcode}{calls}++;
$accountcodes{$accountcode}{duration} += $detail->duration;
- push @{$accountcodes{$accountcode}{ext_description}},
- $detail->formatted('format' => $format);
+ push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
}
}
foreach my $l ( values %accountcodes ) {
$l->{amount} = sprintf( "%.2f", $l->{amount} );
- unshift @{$l->{ext_description}}, $section->{'header'};
+ my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
+ foreach my $sorted_detail ( @sorted_detail ) {
+ push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
+ }
+ delete $l->{detail_temp};
push @lines, $l;
}
sub _items_svc_phone_sections {
my $self = shift;
+ my $conf = $self->conf;
my $escape = shift;
my $format = shift;
my %classnums = ();
my %lines = ();
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
$usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
my $desc = $detail->regionname;
my $description = $desc;
- $description = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
$lines{$phonenum}{$desc} ||= {
description => &{$escape}($description),
sub _items_previous {
my $self = shift;
+ my $conf = $self->conf;
my $cust_main = $self->cust_main;
my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
my @b = ();
? 'due '. $_->due_date2str($date_format)
: time2str($date_format, $_->_date);
push @b, {
- 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
+ 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
#'pkgpart' => 'N/A',
'pkgnum' => 'N/A',
'amount' => sprintf("%.2f", $_->owed),
sub _items_cust_bill_pkg {
my $self = shift;
+ my $conf = $self->conf;
my $cust_bill_pkgs = shift;
my %opt = @_;
my $multisection = $opt{multisection} || '';
my $discount_show_always = 0;
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
my @b = ();
my ($s, $r, $u) = ( undef, undef, undef );
foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
{
+ 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, { %$_ }
+ if $_->{amount} != 0
+ || $discount_show_always
+ || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ ;
+ $_ = undef;
+ }
+ }
+
warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
$cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
if $DEBUG > 1;
)
{
- warn "$me _items_cust_bill_pkg considering display item $display\n"
+ warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
+ $display->billpkgdisplaynum. "\n"
if $DEBUG > 1;
my $type = $display->type;
my $desc = $cust_bill_pkg->desc;
- $desc = substr($desc, 0, 50). '...'
- if $format eq 'latex' && length($desc) > 50;
+ $desc = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
my %details_opt = ( 'format' => $format,
'escape_function' => $escape_function,
my $cust_pkg = $cust_bill_pkg->cust_pkg;
- if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
+ if ( (!$type || $type eq 'S')
+ && ( $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero
+ )
+ )
+ {
warn "$me _items_cust_bill_pkg adding setup\n"
if $DEBUG > 1;
my $description = $desc;
- $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+ $description .= ' Setup'
+ if $cust_bill_pkg->recur != 0
+ || $discount_show_always
+ || $cust_bill_pkg->recur_show_zero;
my @d = ();
unless ( $cust_pkg->part_pkg->hide_svc_detail
if ( $multilocation ) {
my $loc = $cust_pkg->location_label;
- $loc = substr($loc, 0, 50). '...'
- if $format eq 'latex' && length($loc) > 50;
+ $loc = substr($loc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($loc) > $maxlength;
push @d, &{$escape_function}($loc);
}
push @{ $s->{ext_description} }, @d;
} else {
$s = {
+ _is_setup => 1,
description => $description,
#pkgpart => $part_pkg->pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
amount => $cust_bill_pkg->setup,
+ setup_show_zero => $cust_bill_pkg->setup_show_zero,
unit_amount => $cust_bill_pkg->unitsetup,
quantity => $cust_bill_pkg->quantity,
ext_description => \@d,
$description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
" - ". time2str($date_format, $cust_bill_pkg->edate).
")"
- unless $conf->exists('disable_line_item_date_ranges');
+ unless $conf->exists('disable_line_item_date_ranges')
+ || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
my @d = ();
if ( $multilocation ) {
my $loc = $cust_pkg->location_label;
- $loc = substr($loc, 0, 50). '...'
- if $format eq 'latex' && length($loc) > 50;
+ $loc = substr($loc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($loc) > $maxlength;
push @d, &{$escape_function}($loc);
}
#pkgpart => $part_pkg->pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
amount => $amount,
+ recur_show_zero => $cust_bill_pkg->recur_show_zero,
unit_amount => $cust_bill_pkg->unitrecur,
quantity => $cust_bill_pkg->quantity,
ext_description => \@d,
#pkgpart => $part_pkg->pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
amount => $amount,
+ recur_show_zero => $cust_bill_pkg->recur_show_zero,
unit_amount => $cust_bill_pkg->unitrecur,
quantity => $cust_bill_pkg->quantity,
ext_description => \@d,
$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, { %$_ }
- if $_->{amount} != 0
- || $discount_show_always
- || $cust_bill_pkg->recur_show_zero;
- $_ = undef;
- }
- }
-
}
- #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, { %$_ }
- # if $_->{amount} != 0
- # || $discount_show_always
- # }
- #}
+ 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, { %$_ }
+ if $_->{amount} != 0
+ || $discount_show_always
+ || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ }
+ }
warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
if $DEBUG > 1;
#'description' => 'Credit ref\#'. $_->crednum.
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
- 'description' => 'Credit applied '.
+ 'description' => $self->mt('Credit applied').' '.
time2str($date_format,$_->cust_credit->_date). $reason,
'amount' => sprintf("%.2f",$_->amount),
};
#something more elaborate if $_->amount ne ->cust_pay->paid ?
push @b, {
- 'description' => "Payment received ".
+ 'description' => $self->mt('Payment received').' '.
time2str($date_format,$_->cust_pay->_date ),
'amount' => sprintf("%.2f", $_->amount )
};
=cut
sub due_date_sql {
+ my $conf = new FS::Conf;
'COALESCE(
SUBSTRING(
COALESCE(
push @search, "cust_main.agentnum = $1";
}
+ #agentnum
+ if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
+ push @search, "cust_bill.custnum = $1";
+ }
+
#_date
if ( $param->{_date} ) {
my($beginning, $ending) = @{$param->{_date}};