use Cwd;
use FS::UID;
use FS::Record qw( qsearch qsearchs );
+use FS::Conf;
use FS::Misc qw( generate_ps generate_pdf );
use FS::pkg_category;
use FS::pkg_class;
+use FS::invoice_mode;
use FS::L10N;
$DEBUG = 0;
$date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
} );
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item conf [ MODE ]
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
+
+If the "mode" pseudo-field is set on the object, the configuration handle
+will be an L<FS::invoice_conf> for that invoice mode (and the customer's
+locale).
+
+=cut
+
+sub conf {
+ my $self = shift;
+ my $mode = $self->get('mode');
+ if ($self->{_conf} and !defined($mode)) {
+ return $self->{_conf};
+ }
+
+ my $cust_main = $self->cust_main;
+ my $locale = $cust_main ? $cust_main->locale : '';
+ my $conf;
+ if ( $mode ) {
+ if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
+ $mode = $mode->modenum;
+ } elsif ( $mode =~ /\D/ ) {
+ die "invalid invoice mode $mode";
+ }
+ $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
+ if (!$conf) {
+ $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
+ # it doesn't have a locale, but system conf still might
+ $conf->set('locale' => $locale) if $conf;
+ }
+ }
+ # if $mode is unspecified, or if there is no invoice_conf matching this mode
+ # and locale, then use the system config only (but with the locale)
+ $conf ||= FS::Conf->new({ 'locale' => $locale });
+ # cache it
+ return $self->{_conf} = $conf;
+}
+
+=item print_text OPTIONS
Returns an text invoice, as a list of lines.
-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.
+Options can be passed as a hash.
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.
sub print_text {
my $self = shift;
- my( $today, $template, %opt );
+ my %params;
if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
+ %params = %{ shift() };
} else {
- ( $today, $template, %opt ) = @_;
+ %params = @_;
}
- my %params = ( 'format' => 'template' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+ $params{'format'} = 'template'; # for some reason
$self->print_generic( %params );
}
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item print_latex HASHREF
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.
-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.
+Options can be passed as a hash.
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<template>, if specified, is the name of a suffix for alternate invoices.
+This is strongly deprecated; see L<FS::invoice_conf> for the right way to
+customize invoice templates for different purposes.
I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
sub print_latex {
my $self = shift;
- my $conf = $self->conf;
- my( $today, $template, %opt );
+ my %params;
+
if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
+ %params = %{ shift() };
} else {
- ( $today, $template, %opt ) = @_;
+ %params = @_;
}
- my %params = ( 'format' => 'latex' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+ $params{'format'} = 'latex';
+ my $conf = $self->conf;
+ # this needs to go away
+ my $template = $params{'template'};
+ # and this especially
$template ||= $self->_agent_template
if $self->can('_agent_template');
UNLINK => 0,
) or die "can't open temp file: $!\n";
- my $agentnum = $self->cust_main->agentnum;
+ my $agentnum = $self->agentnum;
if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
}
+sub agentnum {
+ my $self = shift;
+ my $cust_main = $self->cust_main;
+ $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
+}
+
=item print_generic OPTION => VALUE ...
Internal method - returns a filled-in template for this invoice as a scalar.
Optional options include
-template - a value used as a suffix for a configuration template
+template - a value used as a suffix for a configuration template. Please
+don't use this.
time - a value used to control the printing of overdue messages. The
default is now. It isn't the date of the invoice; that's the `_date' field.
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;
unless $cust_main->payname
&& $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
+ my $locale = $params{'locale'} || $cust_main->locale;
+
my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
'html' => [ '<%=', '%>' ],
'template' => [ '{', '}' ],
warn "$me print_generic creating template\n"
if $DEBUG > 1;
+ # set the notice name here, and nowhere else.
+ my $notice_name = $params{notice_name}
+ || $conf->config('notice_name')
+ || $self->notice_name;
+
#create the template
my $template = $params{template} ? $params{template} : $self->_agent_template;
my $templatefile = $self->template_conf. $format;
$templatefile .= "_$template"
if length($template) && $conf->exists($templatefile."_$template");
+
+ # the base template
my @invoice_template = map "$_\n", $conf->config($templatefile)
or die "cannot load config data $templatefile";
my $date_format = $date_formats{$format};
- my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
- },
- 'html' => sub { return '<b>'. shift(). '</b>'
- },
- 'template' => sub { shift },
- );
- my $embolden_function = $embolden_functions{$format};
-
my %newline_tokens = ( 'latex' => '\\\\',
'html' => '<br>',
'template' => "\n",
# generate template variables
my $returnaddress;
+
if (
defined( $conf->config_orbase( "invoice_${format}returnaddress",
$template
'agent' => &$escape_function($cust_main->agent->agent),
#invoice/quotation info
- 'invnum' => $self->invnum,
+ 'no_number' => $params{'no_number'},
+ 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
'quotationnum' => $self->quotationnum,
- 'date' => time2str($date_format, $self->_date),
+ 'no_date' => $params{'no_date'},
+ '_date' => ( $params{'no_date'} ? '' : $self->_date ),
+ 'date' => ( $params{'no_date'}
+ ? ''
+ : time2str($date_format, $self->_date)
+ ),
'today' => time2str($date_format_long, $today),
'terms' => $self->terms,
'template' => $template, #params{'template'},
- 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
+ 'notice_name' => $notice_name, # escape?
'current_charges' => sprintf("%.2f", $self->charged),
'duedate' => $self->due_date2str($rdate_format), #date_format?
);
#localization
- my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
+ my $lh = FS::L10N->get_handle( $locale );
$invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
# eval to avoid death for unimplemented languages
my $countrydefault = $conf->config('countrydefault') || 'US';
foreach ( qw( address1 address2 city state zip country fax) ){
my $method = 'ship_'.$_;
- $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+ $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
}
- foreach ( qw( contact company ) ) { #compatibility
- $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
+ if ( length($cust_main->ship_company) ) {
+ $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
+ } else {
+ $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
}
+ $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
$invoice_data{'ship_country'} = ''
if ( $invoice_data{'ship_country'} eq $countrydefault );
#my $balance_due = $self->owed + $pr_total - $cr_total;
my $balance_due = $self->owed + $pr_total;
- # the customer's current balance as shown on the invoice before this one
- $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+ #these are used on the summary page only
+
+ # the customer's current balance as shown on the invoice before this one
+ $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
- # the change in balance from that invoice to this one
- $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
+ # the change in balance from that invoice to this one
+ $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
- # the sum of amount owed on all previous invoices
- $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+ # the sum of amount owed on all previous invoices
+ # ($pr_total is used elsewhere but not as $previous_balance)
+ $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
# the sum of amount owed on all invoices
+ # (this is used in the summary & on the payment coupon)
$invoice_data{'balance'} = sprintf("%.2f", $balance_due);
# info from customer's last invoice before this one, for some
# summary formats
$invoice_data{'last_bill'} = {};
- my $last_bill = $pr_cust_bill[-1];
- if ( $last_bill ) {
- $invoice_data{'last_bill'} = {
- '_date' => $last_bill->_date, #unformatted
- # all we need for now
- };
+
+ if ( $self->custnum && $self->invnum ) {
+
+ if ( $self->previous_bill ) {
+ my $last_bill = $self->previous_bill;
+ $invoice_data{'last_bill'} = {
+ '_date' => $last_bill->_date, #unformatted
+ };
+ my (@payments, @credits);
+ # for formats that itemize previous payments
+ foreach my $cust_pay ( qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => { op => '>=',
+ value => $last_bill->_date }
+ } ) )
+ {
+ next if $cust_pay->_date > $self->_date;
+ push @payments, {
+ '_date' => $cust_pay->_date,
+ 'date' => time2str($date_format, $cust_pay->_date),
+ 'payinfo' => $cust_pay->payby_payinfo_pretty,
+ 'amount' => sprintf('%.2f', $cust_pay->paid),
+ };
+ # not concerned about applications
+ }
+ foreach my $cust_credit ( qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => { op => '>=',
+ value => $last_bill->_date }
+ } ) )
+ {
+ next if $cust_credit->_date > $self->_date;
+ push @credits, {
+ '_date' => $cust_credit->_date,
+ 'date' => time2str($date_format, $cust_credit->_date),
+ 'creditreason'=> $cust_credit->reason,
+ 'amount' => sprintf('%.2f', $cust_credit->amount),
+ };
+ }
+ $invoice_data{'previous_payments'} = \@payments;
+ $invoice_data{'previous_credits'} = \@credits;
+ }
+
}
my $summarypage = '';
my $other_money_char = $other_money_chars{$format};
$invoice_data{'dollar'} = $other_money_char;
+ my %minus_signs = ( 'latex' => '$-$',
+ 'html' => '−',
+ 'template' => '- ' );
+ my $minus = $minus_signs{$format};
+
my @detail_items = ();
my @total_items = ();
my @buf = ();
warn "$me generating sections\n"
if $DEBUG > 1;
- # Previous Charges section
- # subtotal is the first return value from $self->previous
- my $previous_section = { 'description' => $self->mt('Previous Charges'),
- 'subtotal' => $other_money_char.
- sprintf('%.2f', $pr_total),
- 'summarized' => '', #why? $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' => $self->mt('Taxes, Surcharges, and Fees'),
'subtotal' => $taxtotal, # adjusted below
+ 'tax_section' => 1,
};
my $tax_weight = _pkg_category($tax_section->{description})
? _pkg_category($tax_section->{description})->weight
$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
$tax_section->{'sort_weight'} = $tax_weight;
-
my $adjusttotal = 0;
- my $adjust_section = { 'description' =>
- $self->mt('Credits, Payments, and Adjustments'),
- 'subtotal' => 0, # adjusted below
- };
+ my $adjust_section = {
+ 'description' => $self->mt('Credits, Payments, and Adjustments'),
+ 'adjust_section' => 1,
+ 'subtotal' => 0, # adjusted below
+ };
my $adjust_weight = _pkg_category($adjust_section->{description})
? _pkg_category($adjust_section->{description})->weight
: 0;
$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);
+ my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
+ $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
$invoice_data{'multisection'} = $multisection;
- my $late_sections = [];
+ my $late_sections;
my $extra_sections = [];
my $extra_lines = ();
+ # default section ('Charges')
my $default_section = { 'description' => '',
'subtotal' => '',
'no_subtotal' => 1,
};
+ # Previous Charges section
+ # subtotal is the first return value from $self->previous
+ my $previous_section;
+ # if the invoice has major sections, or if we're summarizing previous
+ # charges with a single line, or if we've been specifically told to put them
+ # in a section, create a section for previous charges:
+ if ( $multisection or
+ $conf->exists('previous_balance-summary_only') or
+ $conf->exists('previous_balance-section') ) {
+
+ $previous_section = { 'description' => $self->mt('Previous Charges'),
+ 'subtotal' => $other_money_char.
+ sprintf('%.2f', $pr_total),
+ 'summarized' => '', #why? $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');
+
+ } else {
+ # otherwise put them in the main section
+ $previous_section = $default_section;
+ }
+
if ( $multisection ) {
($extra_sections, $extra_lines) =
$self->_items_extra_usage_sections($escape_function_nonbsp, $format)
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
+
+ # the code is written so that both methods can be used together, but
+ # we haven't yet changed the template to take advantage of that, so for
+ # now, treat them as mutually exclusive.
+ my %section_method = ( by_category => 1 );
+ if ( $conf->exists($tc.'sections_by_location') ) {
+ %section_method = ( by_location => 1 );
+ }
+ my ($early, $late) =
+ $self->_items_sections( 'summary' => $summarypage,
+ 'escape' => $escape_function_nonbsp,
+ 'extra_sections' => $extra_sections,
+ 'format' => $format,
+ %section_method
);
+ push @sections, @$early;
+ $late_sections = $late;
+
if ( $conf->exists('svc_phone_sections')
&& $self->can('_items_svc_phone_sections')
)
# make a default section
push @sections, $default_section;
# and calculate the finance charge total, since it won't get done otherwise.
- # XXX possibly other totals?
+ # and the default section total
# XXX possibly finance_pkgclass should not be used in this manner?
- if ( $conf->exists('finance_pkgclass') ) {
- my @finance_charges;
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
- if ( grep { $_->section eq $invoice_data{finance_section} }
- $cust_bill_pkg->cust_bill_pkg_display ) {
- # I think these are always setup fees, but just to be sure...
- push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
- }
+ my @finance_charges;
+ my @charges;
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ if ( $invoice_data{finance_section} and
+ grep { $_->section eq $invoice_data{finance_section} }
+ $cust_bill_pkg->cust_bill_pkg_display ) {
+ # I think these are always setup fees, but just to be sure...
+ push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
+ } else {
+ push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
}
- $invoice_data{finance_amount} =
- sprintf('%.2f', sum( @finance_charges ) || 0);
}
+ $invoice_data{finance_amount} =
+ sprintf('%.2f', sum( @finance_charges ) || 0);
+ $default_section->{subtotal} = $other_money_char.
+ sprintf('%.2f', sum( @charges ) || 0);
}
# previous invoice balances in the Previous Charges section if there
# is one, otherwise in the main detail section
+ # (except if summary_only is enabled, don't show them at all)
if ( $self->can('_items_previous') &&
$self->enable_previous &&
! $conf->exists('previous_balance-summary_only') ) {
foreach my $line_item ( $self->_items_previous ) {
my $detail = {
- ext_description => [],
+ ref => $line_item->{'pkgnum'},
+ pkgpart => $line_item->{'pkgpart'},
+ quantity => 1,
+ section => $previous_section, # which might be $default_section
+ description => &$escape_function($line_item->{'description'}),
+ ext_description => [ map { &$escape_function($_) }
+ @{ $line_item->{'ext_description'} || [] }
+ ],
+ amount => ( $old_latex ? '' : $money_char).
+ $line_item->{'amount'},
+ product_code => $line_item->{'pkgpart'} || 'N/A',
};
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'pkgpart'} = $line_item->{'pkgpart'};
- $detail->{'quantity'} = 1;
- $detail->{'section'} = $multisection ? $previous_section
- : $default_section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = map {
- &$escape_function($_);
- } @{$line_item->{'ext_description'}};
- }
- $detail->{'amount'} = ( $old_latex ? '' : $money_char).
- $line_item->{'amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
push @detail_items, $detail;
push @buf, [ $detail->{'description'},
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
$options{'summary_page'} = $summarypage;
$options{'skip_usage'} =
scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
- $options{'multisection'} = $multisection;
warn "$me searching for line items\n"
if $DEBUG > 1;
$detail->{'sdate'} = $line_item->{'sdate'};
$detail->{'edate'} = $line_item->{'edate'};
$detail->{'seconds'} = $line_item->{'seconds'};
+ $detail->{'svc_label'} = $line_item->{'svc_label'};
push @detail_items, $detail;
push @buf, ( [ $detail->{'description'},
$invoice_data{current_less_finance} =
sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
- # create a major section for previous balance if we have major sections,
- # or if previous_section is in summary form
- if ( ( $multisection && $self->enable_previous )
- || $conf->exists('previous_balance-summary_only') )
- {
- unshift @sections, $previous_section if $pr_total;
+ # if there's anything in the Previous Charges section, prepend it to the list
+ if ( $pr_total and $previous_section ne $default_section ) {
+ unshift @sections, $previous_section;
}
warn "$me adding taxes\n"
if $DEBUG > 1;
- foreach my $tax ( $self->_items_tax ) {
+ my @items_tax = $self->_items_tax;
+ foreach my $tax ( @items_tax ) {
$taxtotal += $tax->{'amount'};
}
- if ( $taxtotal ) {
+ if ( @items_tax ) {
my $total = {};
$total->{'total_item'} = $self->mt('Sub-total');
$total->{'total_amount'} =
$money_char. sprintf("%10.2f",$self->charged) ];
push @buf,['',''];
- # calculate total, possibly including total owed on previous
- # invoices
- {
+
+ ###
+ # Totals
+ ###
+
+ my %embolden_functions = (
+ 'latex' => sub { return '\textbf{'. shift(). '}' },
+ 'html' => sub { return '<b>'. shift(). '</b>' },
+ 'template' => sub { shift },
+ );
+ my $embolden_function = $embolden_functions{$format};
+
+ if ( $self->can('_items_total') ) { # quotations
+
+ $self->_items_total(\@total_items);
+
+ foreach ( @total_items ) {
+ $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
+ $_->{'total_amount'} = &$embolden_function( $other_money_char.
+ $_->{'total_amount'}
+ );
+ }
+
+ } else { #normal invoice case
+
+ # calculate total, possibly including total owed on previous
+ # invoices
my $total = {};
my $item = 'Total';
$item = $conf->config('previous_balance-exclude_from_total')
$adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
$other_money_char. sprintf('%.2f', $self->charged );
}
- }else{
+ } else {
push @total_items, $total;
}
push @buf,['','-----------'];
sprintf( '%10.2f', $amount )
];
push @buf,['',''];
- }
- # if we're showing previous invoices, also show previous
- # credits and payments
- if ( $self->enable_previous
- and $self->can('_items_credits')
- and $self->can('_items_payments') )
- {
- #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-
- # credits
- my $credittotal = 0;
- foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+ # if we're showing previous invoices, also show previous
+ # credits and payments
+ if ( $self->enable_previous
+ and $self->can('_items_credits')
+ and $self->can('_items_payments') )
+ {
+ #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+
+ # credits
+ my $credittotal = 0;
+ foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
+ my $total;
+ $total->{'total_item'} = &$escape_function($credit->{'description'});
+ $credittotal += $credit->{'amount'};
+ $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
+ $adjusttotal += $credit->{'amount'};
+ if ( $multisection ) {
+ my $money = $old_latex ? '' : $money_char;
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => &$escape_function($credit->{'description'}),
+ amount => $money. $credit->{'amount'},
+ product_code => '',
+ section => $adjust_section,
+ };
+ } else {
+ push @total_items, $total;
+ }
- my $total;
- $total->{'total_item'} = &$escape_function($credit->{'description'});
- $credittotal += $credit->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
- $adjusttotal += $credit->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($credit->{'description'}),
- amount => $money. $credit->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- } else {
- push @total_items, $total;
}
+ $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
- }
- $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
- #credits (again)
- foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
- push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
- }
+ #credits (again)
+ foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+ push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+ }
- # payments
- my $paymenttotal = 0;
- foreach my $payment ( $self->_items_payments ) {
- my $total = {};
- $total->{'total_item'} = &$escape_function($payment->{'description'});
- $paymenttotal += $payment->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
- $adjusttotal += $payment->{'amount'};
+ # payments
+ my $paymenttotal = 0;
+ foreach my $payment ( $self->_items_payments ) {
+ my $total = {};
+ $total->{'total_item'} = &$escape_function($payment->{'description'});
+ $paymenttotal += $payment->{'amount'};
+ $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
+ $adjusttotal += $payment->{'amount'};
+ if ( $multisection ) {
+ my $money = $old_latex ? '' : $money_char;
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => &$escape_function($payment->{'description'}),
+ amount => $money. $payment->{'amount'},
+ product_code => '',
+ section => $adjust_section,
+ };
+ }else{
+ push @total_items, $total;
+ }
+ push @buf, [ $payment->{'description'},
+ $money_char. sprintf("%10.2f", $payment->{'amount'}),
+ ];
+ }
+ $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
+
if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($payment->{'description'}),
- amount => $money. $payment->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- }else{
- push @total_items, $total;
+ $adjust_section->{'subtotal'} = $other_money_char.
+ sprintf('%.2f', $adjusttotal);
+ push @sections, $adjust_section
+ unless $adjust_section->{sort_weight};
}
- push @buf, [ $payment->{'description'},
- $money_char. sprintf("%10.2f", $payment->{'amount'}),
- ];
- }
- $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-
- if ( $multisection ) {
- $adjust_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $adjusttotal);
- push @sections, $adjust_section
- unless $adjust_section->{sort_weight};
- }
- # create Balance Due message
- {
- my $total;
- $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char. sprintf('%.2f', $summarypage
- ? $self->charged +
- $self->billing_balance
- : $self->owed + $pr_total
- )
- );
- if ( $multisection && !$adjust_section->{sort_weight} ) {
- $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
- $total->{'total_amount'};
- }else{
- push @total_items, $total;
+ # create Balance Due message
+ {
+ my $total;
+ $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+ $total->{'total_amount'} =
+ &$embolden_function(
+ $other_money_char. sprintf('%.2f', #why? $summarypage
+ # ? $self->charged +
+ # $self->billing_balance
+ # :
+ $self->owed + $pr_total
+ )
+ );
+ if ( $multisection && !$adjust_section->{sort_weight} ) {
+ $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+ $total->{'total_amount'};
+ }else{
+ push @total_items, $total;
+ }
+ push @buf,['','-----------'];
+ push @buf,[$self->balance_due_msg, $money_char.
+ sprintf("%10.2f", $balance_due ) ];
}
- push @buf,['','-----------'];
- 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;
+ 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 ) ];
}
- push @buf,['','-----------'];
- push @buf,[$self->credit_balance_msg, $money_char.
- sprintf("%10.2f", -$cust_main->balance ) ];
}
- }
+
+ } #end of default total adding ! can('_items_total')
if ( $multisection ) {
if ( $conf->exists('svc_phone_sections')
} } @discounts_avail;
}
+ my @summary_subtotals;
+ # the templates say "$_->{tax_section} || !$_->{summarized}"
+ # except 'summarized' is only true when tax_section is true, so this
+ # is always true, so what's the deal?
+ foreach my $s (@sections) {
+ # not to include in the "summary of new charges" block:
+ # finance charges, adjustments, previous charges,
+ # and itemized phone usage sections
+ if ( $s eq $adjust_section or
+ ($s eq $previous_section and $s ne $default_section) or
+ ($invoice_data{'finance_section'} and
+ $invoice_data{'finance_section'} eq $s->{description}) or
+ $s->{'description'} =~ /^\d+ $/ ) {
+ next;
+ }
+ push @summary_subtotals, $s;
+ }
+ $invoice_data{summary_subtotals} = \@summary_subtotals;
+
# debugging hook: call this with 'diag' => 1 to just get a hash of
# the invoice variables
return \%invoice_data if ( $params{'diag'} );
my %params;
if ( ref($_[0]) ) {
%params = %{ shift() };
- }else{
- $params{'time'} = shift;
- $params{'template'} = shift;
- $params{'cid'} = shift;
+ } else {
+ %params = @_;
}
-
$params{'format'} = 'html';
$self->print_generic( %params );
time2str($date_format, $self->_date);
}
-=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
+=item _items_sections OPTIONS
Generate section information for all items appearing on this invoice.
This will only be called for multi-section invoices.
returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
coderefs to generate parts of the invoice. This is not advised.
-Arguments:
+The method returns two arrayrefs, one of "early" sections and one of "late"
+sections.
+
+OPTIONS may include:
+
+by_location: a flag to divide the invoice into sections by location.
+Each section hash will have a 'location' element containing a hashref of
+the location fields (see L<FS::cust_location>). The section description
+will be the location label, but the template can use any of the location
+fields to create a suitable label.
-LATE: an arrayref to push the "late" section hashes onto. The "early"
-group is simply returned from the method.
+by_category: a flag to divide the invoice into sections using display
+records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
+behavior. Each section hash will have a 'category' element containing
+the section name from the display record (which probably equals the
+category name of the package, but may not in some cases).
-SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
+summary: a flag indicating that this is a summary-format invoice.
Turning this on has the following effects:
- Ignores display items with the 'summary' flag.
-- Combines all items into the "early" group.
+- Places all sections in the "early" group even if they have post_total.
- Creates sections for all non-disabled package categories, even if they
have no charges on this invoice, as well as a section with no name.
-ESCAPE: an escape function to use for section titles.
+escape: an escape function to use for section titles.
-EXTRA_SECTIONS: an arrayref of additional sections to return after the
+extra_sections: an arrayref of additional sections to return after the
sorted list. If there are any of these, section subtotals exclude
usage charges.
-FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
+format: 'latex', 'html', or 'template' (i.e. text). Not used, but
passed through to C<_condense_section()>.
=cut
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 %opt = @_;
+
+ my $escape = $opt{escape};
+ my @extra_sections = @{ $opt{extra_sections} || [] };
+ # $subtotal{$locationnum}{$categoryname} = amount.
+ # if we're not using by_location, $locationnum is undef.
+ # if we're not using by_category, you guessed it, $categoryname is undef.
+ # if we're not using either one, we shouldn't be here in the first place...
my %subtotal = ();
my %late_subtotal = ();
my %not_tax = ();
+ # About tax items + multisection invoices:
+ # If either invoice_*summary option is enabled, AND there is a
+ # package category with the name of the tax, then there will be
+ # a display record assigning the tax item to that category.
+ #
+ # However, the taxes are always placed in the "Taxes, Surcharges,
+ # and Fees" section regardless of that. The only effect of the
+ # display record is to create a subtotal for the summary page.
+
+ # cache these
+ my $pkg_hash = $self->cust_pkg_hash;
+
foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
{
my $usage = $cust_bill_pkg->usage;
+ my $locationnum;
+ if ( $opt{by_location} ) {
+ if ( $cust_bill_pkg->pkgnum ) {
+ $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
+ } else {
+ $locationnum = '';
+ }
+ } else {
+ $locationnum = undef;
+ }
+
+ # as in _items_cust_pkg, if a line item has no display records,
+ # cust_bill_pkg_display() returns a default record for it
+
foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
- next if ( $display->summary && $summarypage );
+ next if ( $display->summary && $opt{summary} );
my $section = $display->section;
my $type = $display->type;
+ $section = undef unless $opt{by_category};
- $not_tax{$section} = 1
+ $not_tax{$locationnum}{$section} = 1
unless $cust_bill_pkg->pkgnum == 0;
# there's actually a very important piece of logic buried in here:
# When _items_cust_bill_pkg is called to generate line items for
# real, it will be called with 'section' => $section for each
# of these.
- if ( $display->post_total && !$summarypage ) {
+ if ( $display->post_total && !$opt{summary} ) {
if (! $type || $type eq 'S') {
- $late_subtotal{$section} += $cust_bill_pkg->setup
+ $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
if $cust_bill_pkg->setup != 0
|| $cust_bill_pkg->setup_show_zero;
}
if (! $type) {
- $late_subtotal{$section} += $cust_bill_pkg->recur
+ $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
if $cust_bill_pkg->recur != 0
|| $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'R') {
- $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+ $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
if $cust_bill_pkg->recur != 0
|| $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'U') {
- $late_subtotal{$section} += $usage
- unless scalar(@$extra_sections);
+ $late_subtotal{$locationnum}{$section} += $usage
+ unless scalar(@extra_sections);
}
- } else {
+ } else { # it's a pre-total (normal) section
+ # skip tax items unless they're explicitly included in a section
next if $cust_bill_pkg->pkgnum == 0 && ! $section;
if (! $type || $type eq 'S') {
- $subtotal{$section} += $cust_bill_pkg->setup
+ $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
if $cust_bill_pkg->setup != 0
|| $cust_bill_pkg->setup_show_zero;
}
if (! $type) {
- $subtotal{$section} += $cust_bill_pkg->recur
+ $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
if $cust_bill_pkg->recur != 0
|| $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'R') {
- $subtotal{$section} += $cust_bill_pkg->recur - $usage
+ $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
if $cust_bill_pkg->recur != 0
|| $cust_bill_pkg->recur_show_zero;
}
if ($type && $type eq 'U') {
- $subtotal{$section} += $usage
- unless scalar(@$extra_sections);
+ $subtotal{$locationnum}{$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;
+ # summary invoices need subtotals for all non-disabled package categories,
+ # even if they're zero
+ # but currently assume that there are no location sections, or at least
+ # that the summary page doesn't care about them
+ if ( $opt{summary} ) {
+ foreach my $category (qsearch('pkg_category', {disabled => ''})) {
+ $subtotal{''}{$category->categoryname} ||= 0;
+ }
+ $subtotal{''}{''} ||= 0;
}
- 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;
-
+ my @sections;
+ foreach my $post_total (0,1) {
+ my @these;
+ my $s = $post_total ? \%late_subtotal : \%subtotal;
+ foreach my $locationnum (keys %$s) {
+ foreach my $sectionname (keys %{ $s->{$locationnum} }) {
+ my $section = {
+ 'subtotal' => $s->{$locationnum}{$sectionname},
+ 'post_total' => $post_total,
+ 'sort_weight' => 0,
+ };
+ if ( $locationnum ) {
+ $section->{'locationnum'} = $locationnum;
+ my $location = FS::cust_location->by_key($locationnum);
+ $section->{'description'} = &{ $escape }($location->location_label);
+ # Better ideas? This will roughly group them by proximity,
+ # which alpha sorting on any of the address fields won't.
+ # Sorting by locationnum is meaningless.
+ # We have to sort on _something_ or the order may change
+ # randomly from one invoice to the next, which will confuse
+ # people.
+ $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
+ $locationnum;
+ $section->{'location'} = {
+ map { $_ => &{ $escape }($location->get($_)) }
+ $location->fields
+ };
+ } else {
+ $section->{'category'} = $sectionname;
+ $section->{'description'} = &{ $escape }($sectionname);
+ if ( _pkg_category($_) ) {
+ $section->{'sort_weight'} = _pkg_category($_)->weight;
+ if ( _pkg_category($_)->condense ) {
+ $section = { %$section, $self->_condense_section($opt{format}) };
+ }
+ }
+ }
+ if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
+ # then it's a tax-only section
+ $section->{'summarized'} = 'Y';
+ $section->{'tax_section'} = 'Y';
+ }
+ push @these, $section;
+ } # foreach $sectionname
+ } #foreach $locationnum
+ push @these, @extra_sections if $post_total == 0;
+ # need an alpha sort for location sections, because postal codes can
+ # be non-numeric
+ $sections[ $post_total ] = [ sort {
+ $opt{'by_location'} ?
+ ($a->{sort_weight} cmp $b->{sort_weight}) :
+ ($a->{sort_weight} <=> $b->{sort_weight})
+ } @these ];
+ } #foreach $post_total
+
+ return @sections; # early, late
}
#helper subs for above
-sub _sectionsort {
- _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+sub cust_pkg_hash {
+ my $self = shift;
+ $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
}
sub _pkg_category {
}
-# sub _items { # seems to be unused
-# my $self = shift;
-#
-# #my @display = scalar(@_)
-# # ? @_
-# # : qw( _items_previous _items_pkg );
-# # #: qw( _items_pkg );
-# # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
-# my @display = qw( _items_previous _items_pkg );
-#
-# my @b = ();
-# foreach my $display ( @display ) {
-# push @b, $self->$display(@_);
-# }
-# @b;
-# }
-
=item _items_pkg [ OPTIONS ]
Return line item hashes for each package item on this invoice. Nearly
=cut
+sub _items_nontax {
+ my $self = shift;
+ grep { $_->pkgnum } $self->cust_bill_pkg;
+}
+
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;
+ my @cust_bill_pkg = $self->_items_nontax;
warn "$me _items_pkg filtering line items\n"
if $DEBUG > 1;
sub _items_tax {
my $self = shift;
my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+ my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+ if ( $self->conf->exists('always_show_tax') ) {
+ my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
+ if (0 == grep { $_->{description} eq $itemdesc } @items) {
+ push @items,
+ { 'description' => $itemdesc,
+ 'amount' => 0.00 };
+ }
+ }
+ @items;
}
=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
DEPRECATED? (expensive, mostly unused?)
format_function: the function used to format CDRs.
-section: a hashref containing 'description'; if this is present,
-cust_bill_pkg_display records not belonging to this section are
-ignored.
+section: a hashref containing 'category' and/or 'locationnum'; if this
+is present, only returns line items that belong to that category and/or
+location (whichever is defined).
multisection: a flag indicating that this is a multisection invoice,
which does something complicated.
my $format_function = $opt{format_function} || '';
my $no_usage = $opt{no_usage} || '';
my $unsquelched = $opt{unsquelched} || ''; #unused
- my $section = $opt{section}->{description} if $opt{section};
+ my ($section, $locationnum, $category);
+ if ( $opt{section} ) {
+ $category = $opt{section}->{category};
+ $locationnum = $opt{section}->{locationnum};
+ }
my $summary_page = $opt{summary_page} || ''; #unused
- my $multisection = $opt{multisection} || '';
+ my $multisection = defined($category) || defined($locationnum);
my $discount_show_always = 0;
my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
}
}
+ if ( $locationnum ) {
+ # this is a location section; skip packages that aren't at this
+ # service location.
+ next if $cust_bill_pkg->pkgnum == 0;
+ next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
+ != $locationnum;
+ }
+
+ # Consider display records for this item to determine if it belongs
+ # in this section. Note that if there are no display records, there
+ # will be a default pseudo-record that includes all charge types
+ # and has no section name.
my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
? $cust_bill_pkg->cust_bill_pkg_display
: ( $cust_bill_pkg );
$cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
if $DEBUG > 1;
- foreach my $display ( grep { defined($section)
- ? $_->section eq $section
- : 1
- }
- grep { !$_->summary || $multisection }
- @cust_bill_pkg_display
- )
- {
+ if ( defined($category) ) {
+ # then this is a package category section; process all display records
+ # that belong to this section.
+ @cust_bill_pkg_display = grep { $_->section eq $category }
+ @cust_bill_pkg_display;
+ } else {
+ # otherwise, process all display records that aren't usage summaries
+ # (I don't think there should be usage summaries if you aren't using
+ # category sections, but this is the historical behavior)
+ @cust_bill_pkg_display = grep { !$_->summary }
+ @cust_bill_pkg_display;
+ }
+ foreach my $display (@cust_bill_pkg_display) {
warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
$display->billpkgdisplaynum. "\n"
my $type = $display->type;
- my $desc = $cust_bill_pkg->desc;
+ my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
$desc = substr($desc, 0, $maxlength). '...'
if $format eq 'latex' && length($desc) > $maxlength;
if $DEBUG > 1;
my $cust_pkg = $cust_bill_pkg->cust_pkg;
+ my $part_pkg = $cust_pkg->part_pkg;
# which pkgpart to show for display purposes?
my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
# start/end dates for invoice formats that do nonstandard
# things with them
- my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
+ my %item_dates = ();
+ %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
+ unless $part_pkg->option('disable_line_item_date_ranges',1);
if ( (!$type || $type eq 'S')
&& ( $cust_bill_pkg->setup != 0
|| $discount_show_always
|| $cust_bill_pkg->recur_show_zero;
+ $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
+ $self->agentnum )
+ if $part_pkg->is_prepaid #for prepaid, "display the validity period
+ # triggered by the recurring charge freq
+ # (RT#26274)
+ && $cust_bill_pkg->recur == 0
+ && ! $cust_bill_pkg->recur_show_zero;
+
my @d = ();
+ my $svc_label;
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')
+ my @svc_labels = map &{$escape_function}($_),
+ $cust_pkg->h_labels_short($self->_date, undef, 'I');
+ push @d, @svc_labels
unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+ $svc_label = $svc_labels[0];
- if ( ! $cust_pkg->locationnum or
- $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
+ my $lnum = $cust_main ? $cust_main->ship_locationnum
+ : $self->prospect_main->locationnum;
+ # show the location label if it's not the customer's default
+ # location, and we're not grouping items by location already
+ if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;
unit_amount => $cust_bill_pkg->unitsetup,
quantity => $cust_bill_pkg->quantity,
ext_description => \@d,
+ svc_label => ($svc_label || ''),
};
};
if $DEBUG > 1;
my $is_summary = $display->summary;
- my $description = ($is_summary && $type && $type eq 'U')
- ? "Usage charges" : $desc;
-
- #pry be a bit more efficient to look some of this conf stuff up
- # outside the loop
- unless (
- $conf->exists('disable_line_item_date_ranges')
- || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
- ) {
- my $time_period;
- my $date_style = $conf->config( 'cust_bill-line_item-date_style',
- $cust_main->agentnum
- );
- if ( defined($date_style) && $date_style eq 'month_of' ) {
- $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
- } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
- my $desc = $conf->config( 'cust_bill-line_item-date_description',
- $cust_main->agentnum
- );
- $desc .= ' ' unless $desc =~ /\s$/;
- $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
- } else {
- $time_period = time2str($date_format, $cust_bill_pkg->sdate).
- " - ". time2str($date_format, $cust_bill_pkg->edate);
- }
- $description .= " ($time_period)";
+ my $description = $desc;
+ if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
+ $description = $self->mt('Usage charges');
}
+ my $part_pkg = $cust_pkg->part_pkg;
+
+ $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
+ $self->agentnum );
+
my @d = ();
my @seconds = (); # for display of usage info
+ my $svc_label = '';
#at least until cust_bill_pkg has "past" ranges in addition to
#the "future" sdate/edate ones... see #3032
push @dates, $prev->sdate if $prev;
push @dates, undef if !$prev;
- unless ( $cust_pkg->part_pkg->hide_svc_detail
+ unless ( $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, 'I')
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate)
+ my @svc_labels = map &{$escape_function}($_),
+ $cust_pkg->h_labels_short(@dates, 'I');
+ push @d, @svc_labels
unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+ $svc_label = $svc_labels[0];
warn "$me _items_cust_bill_pkg done adding service details\n"
if $DEBUG > 1;
- if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
+ my $lnum = $cust_main ? $cust_main->ship_locationnum
+ : $self->prospect_main->locationnum;
+ # show the location label if it's not the customer's default
+ # location, and we're not grouping items by location already
+ if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
my $loc = $cust_pkg->location_label;
$loc = substr($loc, 0, $maxlength). '...'
if $format eq 'latex' && length($loc) > $maxlength;
quantity => $cust_bill_pkg->quantity,
%item_dates,
ext_description => \@d,
+ svc_label => ($svc_label || ''),
};
$r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
}
warn "$me _items_cust_bill_pkg adding usage\n"
if $DEBUG > 1;
- if ( $cust_bill_pkg->hidden ) {
+ if ( $cust_bill_pkg->hidden and defined($u) ) {
+ # if this is a hidden package and there's already a usage
+ # line for the bundle, add this package's total amount and
+ # usage details to it
$u->{amount} += $amount;
$u->{unit_amount} += $unit_amount,
push @{ $u->{ext_description} }, @d;
- } else {
+ } elsif ( $amount ) {
+ # create a new usage line
$u = {
description => $description,
pkgpart => $pkgpart,
%item_dates,
ext_description => \@d,
};
- }
+ } # else this has no usage, so don't create a usage section
}
} # recurring or usage with recurring charge