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;
use FS::pkg_class;
use FS::invoice_mode;
use FS::L10N;
+use FS::Log;
$DEBUG = 0;
$me = '[FS::Template_Mixin]';
=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
'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+ 'couponlocation' => (scalar($conf->config('invoice_latexcouponlocation', $agentnum)) eq "top") ? 'top' : 'bottom',
# better hang on to conf_dir for a while (for old templates)
'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
);
}
- 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 = ();
my %options = ();
$options{'section'} = $section if $multisection;
$options{'section_with_taxes'} = 1
- if $conf->exists('invoice_sections_with_taxes');
+ 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;
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;
#$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
#$tax_section->{'sort_weight'} = $tax_weight;
+ my $invoice_sections_with_taxes = $conf->config_bool(
+ 'invoice_sections_with_taxes', $cust_main->agentnum
+ );
+
foreach my $tax ( @items_tax ) {
- $taxtotal += $tax->{'amount'};
my $description = &$escape_function( $tax->{'description'} );
my $amount = sprintf( '%.2f', $tax->{'amount'} );
if ( $multisection ) {
+ if ( !$invoice_sections_with_taxes ) {
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => $description,
- amount => $money_char. $amount,
- product_code => '',
- section => $tax_section,
- };
+ $taxtotal += $tax->{'amount'};
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => $description,
+ amount => $money_char. $amount,
+ product_code => '',
+ section => $tax_section,
+ };
+
+ }
} else {
+ $taxtotal += $tax->{'amount'};
+
push @total_items, {
'total_item' => $description,
'total_amount' => $other_money_char. $amount,
$other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
if ( $multisection ) {
+
+ if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
+ # If all tax items are displayed in location/category sections,
+ # remove the empty tax section
+ @sections = grep{ $_ ne $tax_section } @sections
+ unless grep{ $_->{section} eq $tax_section } @detail_items;
+ }
+
if ( $taxtotal > 0 ) {
# there are taxes, so prepare the section to be displayed.
# $taxtotal already includes any line items that were already in the
$tax_section->{'description'} = $self->mt($tax_description);
$tax_section->{'summarized'} = '';
- if ( $conf->exists('invoice_sections_with_taxes')) {
+ # append tax section unless it's already there
+ push @sections, $tax_section
+ unless grep {$_ eq $tax_section} @sections;
- # 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;
-
- }
+ push @summary_subtotals, $tax_section
+ unless grep {$_ eq $tax_section} @summary_subtotals;
}
} else {
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)
=back
-Returns an argument list to be passed to L<FS::Misc::send_email>.
+Returns an argument list to be passed to L<FS::Misc/send_email>.
=cut
if (!@text) {
- if ( $conf->config($tc.'template') ) {
+ if ( $conf->exists($tc.'template') ) {
warn "$me generating plain text invoice"
if $DEBUG;
- # 'print_text' argument is no longer used
- @text = map Encode::encode_utf8($_), $self->print_text(\%args);
+ @text = $self->print_text(\%args);
} else {
'Encoding' => 'quoted-printable',
'Charset' => 'UTF-8',
#'Encoding' => '7bit',
- 'Data' => \@text,
+ 'Data' => [
+ map
+ { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) }
+ @text
+ ],
'Disposition' => 'inline',
);
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- Encode::encode_utf8($html),
+ Encode::encode(
+ 'UTF-8',
+ $html,
+ Encode::FB_WARN | Encode::LEAVE_SRC
+ ),
' </body>',
'</html>',
],
sub postal_mail_fsinc {
my ( $self, %opt ) = @_;
+ if ( $FS::Misc::DISABLE_PRINT ) {
+ warn 'postal_mail_fsinc() disabled by $FS::Misc::DISABLE_PRINT' if $DEBUG;
+ return;
+ }
+
my $url = 'https://ws.freeside.biz/print';
my $cust_main = $self->cust_main;
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
+ ? $self->cust_main->locale
+ : $self->prospect_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
warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
next;
}
- if ( exists($options{section}) and exists($options{section}{category}) )
- {
- my $categoryname = $options{section}{category};
- # then filter for items that have that section
- if ( $part_fee->categoryname ne $categoryname ) {
- warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
- next;
- }
- } # otherwise include them all in the main section
- # XXX what to do when sectioning by location?
+
+ # If _items_fee is called while building a sectioned invoice,
+ # - invoice_sections_method: category
+ # Skip fee records that do not match the section category.
+ # - invoice_sections_method: location
+ # Skip fee records always for location sections.
+ # The fee records will be presented in the tax/fee section instead.
+ if (
+ exists( $options{section} )
+ and
+ (
+ (
+ exists( $options{section}{category} )
+ and
+ $part_fee->categoryname ne $options{section}{category}
+ )
+ or
+ exists( $options{section}{location})
+ )
+ ) {
+ warn "skipping fee '".$part_fee->itemdesc.
+ "'--not in section $options{section}{category}\n" if $DEBUG;
+ next;
+ }
my @ext_desc;
my %base_invnums; # invnum => invoice date
$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
push @items,
{ feepart => $cust_bill_pkg->feepart,
+ billpkgnum => $cust_bill_pkg->billpkgnum,
amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
description => $desc,
pkg_tax => \@pkg_tax,
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') ) {
if $DEBUG > 1;
my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+ unless ( $cust_pkg ) {
+ # There is no related row in cust_pkg for this cust_bill_pkg.pkgnum.
+ # This invoice may have been broken by an unusual combination
+ # of manually editing package dates, and aborted package changes
+ # when the manually edited dates used are nonsensical.
+
+ my $error = sprintf
+ 'cust_bill_pkg(billpkgnum:%s) '.
+ 'is missing related row in cust_pkg(pkgnum:%s)! '.
+ 'cust_bill(invnum:%s) is corrupted by bad database data, '.
+ 'and should be investigated',
+ $cust_bill_pkg->billpkgnum,
+ $cust_bill_pkg->pkgnum,
+ $cust_bill_pkg->invnum;
+
+ FS::Log->new('FS::cust_bill_pkg')->critical( $error );
+ warn $error;
+ next;
+ }
+
my $part_pkg = $cust_pkg->part_pkg;
# which pkgpart to show for display purposes?
|| ($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)
$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
}
+=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;