use Cwd;
use FS::UID;
use FS::Misc qw( send_email );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
use FS::Conf;
use FS::Misc qw( generate_ps generate_pdf );
use FS::pkg_category;
=item template
-Dprecated. Used as a suffix for a configuration template. Please
+Deprecated. Used as a suffix for a configuration template. Please
don't use this, it deprecated in favor of more flexible alternatives.
=back
$invoice_data{'cid'} = $params{'cid'}
if $params{'cid'};
- if ( $cust_main->country eq $countrydefault ) {
- $invoice_data{'country'} = '';
- } else {
+ if ( $cust_main->bill_locationnum
+ && $cust_main->bill_location->country ne $countrydefault ) {
$invoice_data{'country'} = &$escape_function($cust_main->bill_country_full);
+ } else {
+ $invoice_data{'country'} = '';
}
my @address = ();
);
}
- if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+ if ( $conf->config_bool('invoice_usesummary', $agentnum) ) {
$invoice_data{'summarypage'} = $summarypage = 1;
}
my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
my $multisection = $self->has_sections;
- $conf->exists($tc.'sections', $cust_main->agentnum) ||
- $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
- $invoice_data{'multisection'} = $multisection;
+ if ( $multisection ) {
+ $invoice_data{multisection} = $conf->config($tc.'sections_method') || 1;
+ }
my $late_sections;
my $extra_sections = [];
my $extra_lines = ();
}
} else {
# subtotal sectioning is the same as for the actual invoice sections
- @summary_subtotals = @sections;
+ @summary_subtotals = grep $_->{subtotal}, @sections;
}
# Hereafter, push sections to both @sections and @summary_subtotals
my %options = ();
$options{'section'} = $section if $multisection;
+ $options{'section_with_taxes'} = 1
+ if $multisection
+ && $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum);
$options{'format'} = $format;
$options{'escape_function'} = $escape_function;
$options{'no_usage'} = 1 unless $unsquelched;
$options{'skip_usage'} =
scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
$options{'preref_callback'} = $params{'preref_callback'};
+ $options{'disable_line_item_date_ranges'} =
+ $conf->exists('disable_line_item_date_ranges');
warn "$me searching for line items\n"
if $DEBUG > 1;
+ my %section_tax_lines;
+ my %seen_tax_lines;
foreach my $line_item ( $self->_items_pkg(%options),
$self->_items_fee(%options) ) {
+ # When bill is sectioned by location, fees may be displayed within the
+ # appropriate location section. Suppress this fee from the taxes/fees
+ # end section, so it doesn't appear to be charged twice and make the
+ # subtotals seem incorrect
+ next
+ if $line_item->{locationnum}
+ && ref $options{section}
+ && !exists $options{section}->{locationnum}
+ && $self->has_sections
+ && $conf->config($tc.'sections_method') eq 'location';
+
warn "$me adding line item ".
join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
if $DEBUG > 1;
}
$line_item->{'ext_description'} ||= [];
+ if ( $options{section_with_taxes} && ref $line_item->{pkg_tax} ) {
+ for my $line_tax ( @{$ line_item->{pkg_tax} } ) {
+
+ # It is rarely possible for the same tax record to be presented here
+ # multiple times. See cust_bill_pkg::_pkg_tax_list for more info
+ next if $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} };
+ $seen_tax_lines{ $line_tax->{billpkgtaxlocationnum} } = 1;
+
+ $section_tax_lines{ $line_tax->{taxname} } += $line_tax->{amount};
+ }
+ }
+
push @detail_items, $line_item;
}
+ # If conf flag invoice_sections_with_taxes:
+ # - Add @detail_items for taxes into each section
+ # - Update section subtotal to include taxes
+ if ( $options{section_with_taxes} && %section_tax_lines ) {
+ for my $taxname ( keys %section_tax_lines ) {
+
+ push @detail_items, {
+ section => $section,
+ amount => sprintf($money_char."%.2f",$section_tax_lines{$taxname}),
+ description => &$escape_function($taxname),
+ };
+
+ # Append taxes to total. If line format resembles "$5.00 to $12.00"
+ # append to the second value.
+
+ # $section->{subtotal} = '$5.00 to 12.00'; # for testing:
+ if ($section->{subtotal} =~ /to/) {
+ my @subtotal = split /\s/, $section->{subtotal};
+ $subtotal[2] =~ s/[^\d\.]//g;
+ $subtotal[2] = sprintf(
+ $money_char."%.2f",
+ ( $subtotal[2] + $section_tax_lines{$taxname} )
+ );
+ $section->{subtotal} = join ' ', @subtotal;
+ } else {
+ $section->{subtotal} =~ s/[^\d\.]//g;
+ $section->{subtotal} = sprintf(
+ $money_char . "%.2f",
+ ( $section->{subtotal} + $section_tax_lines{$taxname} )
+ );
+ }
+
+ }
+ }
+
if ( $section->{'description'} ) {
push @buf, ( ['','-----------'],
[ $section->{'description'}. ' sub-total',
$tax_section->{'description'} = $self->mt($tax_description);
$tax_section->{'summarized'} = '';
- # append it if it's not already there
- if ( !grep $tax_section, @sections ) {
+ if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
+
+ # remove tax section if taxes are itemized within other sections
+ @sections = grep{ $_ ne $tax_section } @sections;
+
+ } elsif ( !grep $tax_section, @sections ) {
+
+ # append it if it's not already there
push @sections, $tax_section;
push @summary_subtotals, $tax_section;
+
}
- }
+ }
} else {
unshift @total_items, $total;
}
my $msg = $self->mt('Balance Due');
return $msg unless $self->terms; # huh?
if ( !$self->conf->exists('invoice_show_prior_due_date')
- or $self->conf->exists('invoice_sections') ) {
+ || $self->has_sections ) {
# if enabled, the due date is shown with Total New Charges (see
# _items_total) and not here
# (yes, or if invoice_sections is enabled; this is just for compatibility)
foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
next if ( $display->summary && $opt{summary} );
- my $section = $display->section;
+ #my $section = $display->section;
+ #false laziness with the method, but for efficiency inside this loop
+ my $section = $display->get('section');
+ if ( !$section && !$cust_bill_pkg->hidden ) {
+ $section = $cust_bill_pkg->get('categoryname'); #cust_bill->cust_bill_pkg added it (XXX quotations / quotation_section)
+ }
+
my $type = $display->type;
# Set $section = undef if we're sectioning by location and this
# line item _has_ a location (i.e. isn't a fee).
my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
my $escape_function = $options{escape_function};
+ my $locale = $self->cust_main->locale;
+
my @items;
foreach my $cust_bill_pkg (@cust_bill_pkg) {
# cache this, so we don't look it up again in every section
$self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
);
}
- my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
+ my $desc = $part_fee->itemdesc_locale($locale);
# but not escape the base description line
+ my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+ if $options{section_with_taxes};
+
push @items,
{ feepart => $cust_bill_pkg->feepart,
amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
description => $desc,
- ext_description => \@ext_desc
+ pkg_tax => \@pkg_tax,
+ ext_description => \@ext_desc,
# sdate/edate?
};
}
multisection: a flag indicating that this is a multisection invoice,
which does something complicated.
+section_with_taxes: Look up and include applied taxes for each record
+
Returns a list of hashrefs, each of which may contain:
pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+ my $agentnum = $self->agentnum;
+
# for location labels: use default location on the invoice date
my $default_locationnum;
if ( $conf->exists('invoice-all_pkg_addresses') ) {
# not normally used, but pass this to the template anyway
$classname = $part_pkg->classname;
+ my @pkg_tax = $cust_bill_pkg->_pkg_tax_list
+ if $opt{section_with_taxes};
+
if ( (!$type || $type eq 'S')
&& ( $cust_bill_pkg->setup != 0
|| $cust_bill_pkg->setup_show_zero
|| ($discount_show_always and $cust_bill_pkg->unitrecur > 0)
|| $cust_bill_pkg->recur_show_zero;
- $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
- $self->agentnum )
+ my $disable_date_ranges =
+ $opt{disable_line_item_date_ranges}
+ || $part_pkg->option('disable_line_item_date_ranges', 1);
+
+ $description .= $cust_bill_pkg->time_period_pretty(
+ $part_pkg,
+ $agentnum,
+ disable_date_ranges => $disable_date_ranges,
+ )
if $part_pkg->is_prepaid #for prepaid, "display the validity period
# triggered by the recurring charge freq
# (RT#26274)
push @{ $s->{ext_description} }, @d;
} else {
$s = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
_is_setup => 1,
description => $description,
pkgpart => $pkgpart,
ext_description => \@d,
svc_label => ($svc_label || ''),
locationnum => $cust_pkg->locationnum, # sure, why not?
+ pkg_tax => \@pkg_tax,
};
};
$description = $self->mt('Usage charges');
}
- my $part_pkg = $cust_pkg->part_pkg;
+ my $disable_date_ranges =
+ $opt{disable_line_item_date_ranges}
+ || $part_pkg->option('disable_line_item_date_ranges', 1);
- $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
- $self->agentnum );
+ $description .= $cust_bill_pkg->time_period_pretty(
+ $part_pkg,
+ $agentnum,
+ disable_date_ranges => $disable_date_ranges,
+ );
my @d = ();
my @seconds = (); # for display of usage info
push @{ $r->{ext_description} }, @d;
} else {
$r = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
description => $description,
pkgpart => $pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
ext_description => \@d,
svc_label => ($svc_label || ''),
locationnum => $cust_pkg->locationnum,
+ pkg_tax => \@pkg_tax,
};
$r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
}
} elsif ( $amount ) {
# create a new usage line
$u = {
+ billpkgnum => $cust_bill_pkg->billpkgnum,
description => $description,
pkgpart => $pkgpart,
pkgnum => $cust_bill_pkg->pkgnum,
%item_dates,
ext_description => \@d,
locationnum => $cust_pkg->locationnum,
+ pkg_tax => \@pkg_tax,
};
} # else this has no usage, so don't create a usage section
}
}
+=item has_sections AGENTNUM
+
+Return true if invoice_sections should be enabled for this bill.
+ (Inherited by both cust_bill and cust_bill_void)
+
+Determination:
+* False if not an invoice
+* True always if conf invoice_sections is enabled
+* True always if sections_by_location is enabled
+* True if conf invoice_sections_multilocation > 1,
+ and location_count >= invoice_sections_multilocation
+* Else, False
+
+=cut
+
+sub has_sections {
+ my ($self, $agentnum) = @_;
+
+ return 0 unless $self->invnum > 0;
+
+ $agentnum ||= $self->agentnum;
+ return 1 if $self->conf->config_bool('invoice_sections', $agentnum);
+ return 1 if $self->conf->exists('sections_by_location', $agentnum);
+
+ my $location_min = $self->conf->config(
+ 'invoice_sections_multilocation', $agentnum,
+ );
+
+ return 1
+ if $location_min
+ && $self->location_count >= $location_min;
+
+ 0;
+}
+
+
+=item location_count
+
+Return the number of locations billed on this invoice
+
+=cut
+
+sub location_count {
+ my ($self) = @_;
+ return 0 unless $self->invnum;
+
+ # SELECT COUNT( DISTINCT cust_pkg.locationnum )
+ # FROM cust_bill_pkg
+ # LEFT JOIN cust_pkg USING (pkgnum)
+ # WHERE invnum = 278
+ # AND cust_bill_pkg.pkgnum > 0
+
+ my $result = qsearchs({
+ select => 'COUNT(DISTINCT cust_pkg.locationnum) as location_count',
+ table => 'cust_bill_pkg',
+ addl_from => 'LEFT JOIN cust_pkg USING (pkgnum)',
+ extra_sql => 'WHERE invnum = '.dbh->quote( $self->invnum )
+ . ' AND cust_bill_pkg.pkgnum > 0'
+ });
+ ref $result ? $result->location_count : 0;
+}
+
+
+
1;