package FS::cust_bill;
use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
+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 List::Util qw(min max);
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::payby;
use FS::bill_batch;
use FS::cust_bill_batch;
+use Cwd;
@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') || '$';
- $date_format = $conf->config('date_format') || '%x';
- $rdate_format = $conf->config('date_format') || '%m/%d/%Y';
+ $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
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
}
-=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;
'';
}
'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',
# 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';
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);
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 OPTION => VALUE ...
'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";
@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";
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};
+ 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 = (
#invoice info
'invnum' => $self->invnum,
'date' => time2str($date_format, $self->_date),
- 'today' => time2str('%b %o, %Y', $today),
+ 'today' => time2str($date_format_long, $today),
'terms' => $self->terms,
'template' => $template, #params{'template'},
'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
'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;
+ }
+
+ $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') ) {
$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
}
$invoice_data{'summarypage'} = $summarypage;
- #do variable substitution in notes, footer, smallfooter
+ warn "$me substituting variables in notes, footer, smallfooter\n"
+ if $DEBUG > 1;
+
foreach my $include (qw( notes footer smallfooter coupon )) {
my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
$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),
my $extra_lines = ();
if ( $multisection ) {
($extra_sections, $extra_lines) =
- $self->_items_extra_usage_sections($escape_function, $format)
+ $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 @sections,
$self->_items_sections( $late_sections, # this could stand a refactor
$summarypage,
- $escape_function,
+ $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, $format);
+ $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
push @{$late_sections}, @$phone_sections;
push @detail_items, @$phone_lines;
}
)
{
+ 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
);
}
+ 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{'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 => [],
};
unshift @sections, $previous_section if $pr_total;
}
+ warn "$me adding taxes\n"
+ if $DEBUG > 1;
+
foreach my $tax ( $self->_items_tax ) {
$taxtotal += $tax->{'amount'};
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($file.'.tex');
- unlink($lfile);
+ unlink($logofile);
+ unlink($barcodefile);
$ps;
}
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($file.'.tex');
- unlink($lfile);
+ unlink($logofile);
+ unlink($barcodefile);
$pdf;
}
}
$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 {
}
+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;
push @lines, $l;
}
}
+
+ if($conf->exists('phone_usage_class_summary')) {
+ # this only works with Latex
+ my @newlines;
+ my @newsections;
+
+ # after this, we'll have only two sections per DID:
+ # Calls Summary and Calls Detail
+ foreach my $section ( @sections ) {
+ if($section->{'post_total'}) {
+ $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+ $section->{'total_line_generator'} = sub { '' };
+ $section->{'total_generator'} = sub { '' };
+ $section->{'header_generator'} = sub { '' };
+ $section->{'description_generator'} = '';
+ push @newsections, $section;
+ my %calls_detail = %$section;
+ $calls_detail{'post_total'} = '';
+ $calls_detail{'sort_weight'} = '';
+ $calls_detail{'description_generator'} = sub { '' };
+ $calls_detail{'header_generator'} = sub {
+ return ' & Date/Time & Called Number & Duration & Price'
+ if $format eq 'latex';
+ '';
+ };
+ $calls_detail{'description'} = 'Calls Detail: '
+ . $section->{'phonenum'};
+ push @newsections, \%calls_detail;
+ }
+ }
+
+ # after this, each usage class is collapsed/summarized into a single
+ # line under the Calls Summary section
+ foreach my $newsection ( @newsections ) {
+ if($newsection->{'post_total'}) { # this means Calls Summary
+ foreach my $section ( @sections ) {
+ next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+ && !$section->{'post_total'});
+ my $newdesc = $section->{'description'};
+ my $tn = $section->{'phonenum'};
+ $newdesc =~ s/$tn//g;
+ my $line = { ext_description => [],
+ pkgnum => '',
+ ref => '',
+ quantity => '',
+ calls => $section->{'calls'},
+ section => $newsection,
+ duration => $section->{'duration'},
+ description => $newdesc,
+ amount => sprintf("%.2f",$section->{'amount'}),
+ product_code => 'N/A',
+ };
+ push @newlines, $line;
+ }
+ }
+ }
+
+ # after this, Calls Details is populated with all CDRs
+ foreach my $newsection ( @newsections ) {
+ if(!$newsection->{'post_total'}) { # this means Calls Details
+ foreach my $line ( @lines ) {
+ next unless (scalar(@{$line->{'ext_description'}}) &&
+ $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
+ );
+ my @extdesc = @{$line->{'ext_description'}};
+ my @newextdesc;
+ foreach my $extdesc ( @extdesc ) {
+ $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
+ push @newextdesc, $extdesc;
+ }
+ $line->{'ext_description'} = \@newextdesc;
+ $line->{'section'} = $newsection;
+ push @newlines, $line;
+ }
+ }
+ }
+
+ return(\@newsections, \@newlines);
+ }
return(\@sections, \@lines);
sub _items_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;
+
+ 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 ) {
}
keys %itemshash;
}
+
+ warn "$me _items_pkg returning ". scalar(@items). " items\n"
+ if $DEBUG > 1;
+
@items;
}
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 $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 )
{
+ 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, { %$_ }
- unless $_->{amount} == 0;
+ unless ( $_->{amount} == 0 && !$discount_show_always );
$_ = undef;
}
}
)
{
+ warn "$me _items_cust_bill_pkg considering display item $display\n"
+ if $DEBUG > 1;
+
my $type = $display->type;
my $desc = $cust_bill_pkg->desc;
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;
unless ( $cust_pkg->part_pkg->hide_svc_detail
|| $cust_bill_pkg->hidden )
{
+
push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date);
+ $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($desc, 0, 50). '...'
+ $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 || $cust_bill_pkg->setup == 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 && $type && $type eq 'U')
? "Usage charges" : $desc;
- unless ( $conf->exists('disable_line_item_date_ranges') ) {
- $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
- " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
- }
+ $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;
unless ( $cust_pkg->part_pkg->hide_svc_detail
|| $cust_bill_pkg->itemdesc
|| $cust_bill_pkg->hidden
|| $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)
+ $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($desc, 0, 50). '...'
+ $loc = substr($loc, 0, 50). '...'
if $format eq 'latex' && length($loc) > 50;
push @d, &{$escape_function}($loc);
}
+
}
+ warn "$me _items_cust_bill_pkg adding details\n"
+ if $DEBUG > 1;
+
push @d, $cust_bill_pkg->details(%details_opt)
unless ($is_summary || $type && $type eq 'R');
+
+ warn "$me _items_cust_bill_pkg calculating amount\n"
+ if $DEBUG > 1;
my $amount = 0;
if (!$type) {
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;
} else { # $type eq 'U'
+ warn "$me _items_cust_bill_pkg adding usage\n"
+ if $DEBUG > 1;
+
if ( $cust_bill_pkg->hidden ) {
$u->{amount} += $amount;
$u->{unit_amount} += $cust_bill_pkg->unitrecur;
} 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,
}
+ 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, { %$_ }
- unless $_->{amount} == 0;
+ unless ( $_->{amount} == 0 && !$discount_show_always );
}
}