cust_event
cust_credit_bill
cust_bill_pay
- cust_bill_pay
cust_credit_bill
cust_pay_batch
cust_bill_pay_batch
# eval to avoid death for unimplemented languages
my $dh = eval { Date::Language->new($info{'name'}) } ||
Date::Language->new(); # fall back to English
- $invoice_data{'time2str'} = sub { $dh->time2str(@_) };
+ # prototype here to silence warnings
+ $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
# eventually use this date handle everywhere in here, too
my $min_sdate = 999999999999;
my $previous_section = { 'description' => $self->mt('Previous Charges'),
'subtotal' => $other_money_char.
sprintf('%.2f', $pr_total),
- 'summarized' => $summarypage ? 'Y' : '',
+ 'summarized' => '', #why? $summarypage ? 'Y' : '',
};
$previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
join(' / ', map { $cust_main->balance_date_range(@$_) }
my $taxtotal = 0;
my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
'subtotal' => $taxtotal, # adjusted below
- 'summarized' => $summarypage ? 'Y' : '',
};
my $tax_weight = _pkg_category($tax_section->{description})
? _pkg_category($tax_section->{description})->weight
: 0;
- $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
+ $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
$tax_section->{'sort_weight'} = $tax_weight;
my $adjust_section = { 'description' =>
$self->mt('Credits, Payments, and Adjustments'),
'subtotal' => 0, # adjusted below
- 'summarized' => $summarypage ? 'Y' : '',
};
my $adjust_weight = _pkg_category($adjust_section->{description})
? _pkg_category($adjust_section->{description})->weight
: 0;
- $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
+ $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
$adjust_section->{'sort_weight'} = $adjust_weight;
my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
}
} else {# not multisection
# make a default section
- push @sections, { 'description' => '', 'subtotal' => '' };
+ push @sections, { 'description' => '', 'subtotal' => '',
+ 'no_subtotal' => 1 };
# and calculate the finance charge total, since it won't get done otherwise.
# XXX possibly other totals?
# XXX possibly finance_pkgclass should not be used in this manner?
my ($didsummary,$minutes) = $self->_did_summary;
my $didsummary_desc = 'DID Activity Summary (since last invoice)';
push @detail_items,
- { 'description' => $didsummary_desc,
- 'ext_description' => [ $didsummary, $minutes ],
- };
+ { 'description' => $didsummary_desc,
+ 'ext_description' => [ $didsummary, $minutes ],
+ };
}
foreach my $section (@sections, @$late_sections) {
$detail->{'sdate'} = $line_item->{'sdate'};
$detail->{'edate'} = $line_item->{'edate'};
+ $detail->{'seconds'} = $line_item->{'seconds'};
push @detail_items, $detail;
push @buf, ( [ $detail->{'description'},
}
}
-
+
$invoice_data{current_less_finance} =
sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
unless $adjust_section->{sort_weight};
}
+ # create Balance Due message
{
my $total;
$total->{'total_item'} = &$embolden_function($self->balance_due_msg);
if $unsquelched;
}
+ # make a discounts-available section, even without multisection
+ if ( $conf->exists('discount-show_available')
+ and my @discounts_avail = $self->_items_discounts_avail ) {
+ my $discount_section = {
+ 'description' => $self->mt('Discounts Available'),
+ 'subtotal' => '',
+ 'no_subtotal' => 1,
+ };
+
+ push @sections, $discount_section;
+ push @detail_items, map { +{
+ 'ref' => '', #should this be something else?
+ 'section' => $discount_section,
+ 'description' => &$escape_function( $_->{description} ),
+ 'amount' => $money_char . &$escape_function( $_->{amount} ),
+ 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
+ } } @discounts_avail;
+ }
+
+ # All sections and items are built; now fill in templates.
my @includelist = ();
push @includelist, 'summary' if $summarypage;
foreach my $include ( @includelist ) {
}
#setup subroutine for the template
- #sub FS::cust_bill::_template::invoice_lines { # good god, no
- $invoice_data{invoice_lines} = sub { # much better
+ $invoice_data{invoice_lines} = sub {
my $lines = shift || scalar(@buf);
map {
scalar(@buf)
time2str($date_format, $self->_date);
}
-# I like how _date_pretty was documented but this one wasn't.
-
=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
Generate section information for all items appearing on this invoice.
returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
coderefs to generate parts of the invoice. This is not advised.
-Takes way too many arguments, all mandatory:
+Arguments:
LATE: an arrayref to push the "late" section hashes onto. The "early"
-group is simply returned from the method. Yes, I know. Don't ask.
+group is simply returned from the method.
SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
Turning this on has the following effects:
- 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. Why not just
-let the calling environment escape things itself? Beats the heck out
-of me.
+ESCAPE: an escape function to use for section titles.
EXTRA_SECTIONS: an arrayref of additional sections to return after the
sorted list. If there are any of these, section subtotals exclude
push @d, &{$escape_function}($loc);
}
- }
+ } #unless hiding service details
push @d, $cust_bill_pkg->details(%details_opt)
if $cust_bill_pkg->recur == 0;
|| $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
my @d = ();
+ my @seconds = (); # for display of usage info
#at least until cust_bill_pkg has "past" ranges in addition to
#the "future" sdate/edate ones... see #3032
push @d, &{$escape_function}($loc);
}
+ # Display of seconds_since_sqlradacct:
+ # On the invoice, when processing @detail_items, look for a field
+ # named 'seconds'. This will contain total seconds for each
+ # service, in the same order as @ext_description. For services
+ # that don't support this it will show undef.
+ if ( $conf->exists('svc_acct-usage_seconds')
+ and ! $cust_bill_pkg->pkgpart_override ) {
+ foreach my $cust_svc (
+ $cust_pkg->h_cust_svc(@dates, 'I')
+ ) {
+
+ # eval because not having any part_export_usage exports
+ # is a fatal error, last_bill/_date because that's how
+ # sqlradius_hour billing does it
+ my $sec = eval {
+ $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
+ };
+ push @seconds, $sec;
+ }
+ } #if svc_acct-usage_seconds
+
}
unless ( $is_summary ) {
%item_dates,
ext_description => \@d,
};
+ $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
}
} else { # $type eq 'U'
}
+=item _items_discounts_avail
+
+Returns an array of line item hashrefs representing available term discounts
+for this invoice. This makes the same assumptions that apply to term
+discounts in general: that the package is billed monthly, at a flat rate,
+with no usage charges. A prorated first month will be handled, as will
+a setup fee if the discount is allowed to apply to setup fees.
+
+=cut
+
+sub _items_discounts_avail {
+ my $self = shift;
+ my %terms;
+ my $list_pkgnums = 0; # if any packages are not eligible for all discounts
+
+ my ($previous_balance) = $self->previous;
+
+ foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
+ $terms{$_->months} = {
+ pkgnums => [],
+ base => $previous_balance || 0, # pre-discount sum of charges
+ discounted => $previous_balance || 0, # post-discount sum
+ list_pkgnums => 0, # whether any packages are not discounted
+ }
+ }
+ foreach my $months (keys %terms) {
+ my $hash = $terms{$months};
+
+ # tricky, because packages may not all be eligible for the same discounts
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
+ my $part_pkg = $cust_pkg->part_pkg or next;
+
+ next if $part_pkg->freq ne '1';
+ my $setup = $cust_bill_pkg->setup || 0;
+ my $recur = $cust_bill_pkg->recur || 0;
+ my $permonth = $part_pkg->base_recur_permonth || 0;
+
+ my ($discount) = grep { $_->months == $months }
+ map { $_->discount } $part_pkg->part_pkg_discount;
+
+ $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
+ if ($discount) {
+
+ my $discountable;
+ if ( $discount->setup ) {
+ $discountable += $setup;
+ }
+ else {
+ $hash->{discounted} += $setup;
+ }
+
+ if ( $discount->percent ) {
+ $discountable += $months * $permonth;
+ $discountable -= ($discountable * $discount->percent / 100);
+ $discountable -= ($permonth - $recur); # correct for prorate
+ $hash->{discounted} += $discountable;
+ }
+ else {
+ $discountable += $recur;
+ $discountable -= $discount->amount * $recur/$permonth;
+
+ $discountable += ($months - 1) * max($permonth - $discount->amount,0);
+ }
+
+ $hash->{discounted} += $discountable;
+ push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
+ }
+ else { #no discount
+ $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
+ $hash->{list_pkgnums} = 1;
+ }
+ } #foreach $cust_bill_pkg
+
+ # don't show this line if no packages have discounts at this term
+ # or if there are no new charges to apply the discount to
+ delete $terms{$months} if $hash->{base} == $hash->{discounted}
+ or $hash->{base} == 0;
+
+ }
+
+ $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
+
+ foreach my $months (keys %terms) {
+ my $hash = $terms{$months};
+ my $term_total = sprintf('%.2f', $hash->{discounted});
+ # possibly shouldn't include previous balance in these?
+ my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
+ my $permonth = sprintf('%.2f', $term_total / $months);
+
+ $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
+ $percent, $months
+ );
+ $hash->{amount} = $self->mt('[_1] ([_2] per month)',
+ $term_total, $money_char.$permonth
+ );
+
+ my @detail;
+ if ( $list_pkgnums ) {
+ push @detail, $self->mt('discount on item'). ' '.
+ join(', ', map { "#$_" } @{ $hash->{pkgnums} });
+ }
+ $hash->{ext_description} = join ', ', @detail;
+ }
+
+ map { $terms{$_} } sort {$b <=> $a} keys %terms;
+}
+
=item call_details [ OPTION => VALUE ... ]
Returns an array of CSV strings representing the call details for this invoice