summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormark <mark>2011-10-01 02:25:05 +0000
committermark <mark>2011-10-01 02:25:05 +0000
commitcd3ed01671d5e1f9332c494abbf4bb110aabe1e3 (patch)
tree22c4ecf0dfa716204a1e6acbbb79e24797e82e1d
parenta85bf2b444454d120a334d6ae11a0e981bd75db9 (diff)
show available term discounts on invoice, #14210
-rw-r--r--FS/FS/cust_bill.pm151
-rw-r--r--FS/FS/part_pkg/discount_Mixin.pm43
-rw-r--r--FS/FS/part_pkg_discount.pm2
-rw-r--r--conf/invoice_html2
-rw-r--r--conf/invoice_latex2
5 files changed, 146 insertions, 54 deletions
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 1d3ddb8..bd8c1df 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -2662,7 +2662,8 @@ sub print_generic {
# 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;
@@ -2924,7 +2925,8 @@ sub print_generic {
}
} 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?
@@ -2990,9 +2992,9 @@ sub print_generic {
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) {
@@ -3088,7 +3090,7 @@ sub print_generic {
}
}
-
+
$invoice_data{current_less_finance} =
sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
@@ -3267,6 +3269,7 @@ sub print_generic {
unless $adjust_section->{sort_weight};
}
+ # create Balance Due message
{
my $total;
$total->{'total_item'} = &$embolden_function($self->balance_due_msg);
@@ -3327,6 +3330,26 @@ sub print_generic {
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 ) {
@@ -3389,8 +3412,7 @@ sub print_generic {
}
#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)
@@ -5147,6 +5169,119 @@ sub _items_payments {
}
+=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 %total;
+ my $pkgnums = 0;
+ my $pkgnums_times_discounts = 0;
+ # 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;
+ # for simplicity, skip all this if the customer already has a term discount
+ return () if $cust_pkg->cust_pkg_discount_active;
+
+ $pkgnums++;
+ next if $part_pkg->freq ne '1';
+
+ foreach my $discount (
+ map { $_->discount } $part_pkg->part_pkg_discount
+ ) {
+
+ $total{$discount->discountnum} ||=
+ {
+ discount => $discount,
+ pkgnums => [],
+ base_current => 0,
+ base_permonth => 0,
+ setup_include => 0,
+ setup_exclude => 0,
+ };
+ my $hash = $total{$discount->discountnum};
+ $hash->{discount} = $discount;
+ $hash->{thismonth} += $cust_bill_pkg->recur || 0;
+ $hash->{setup} += $cust_bill_pkg->setup || 0;
+ $hash->{base_permonth} += $part_pkg->base_recur_permonth;
+
+ # and make a list of pkgnums
+ push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
+ $pkgnums_times_discounts++;
+ }
+ }
+
+ # Test for the simple case where all packages on the invoice
+ # are eligible for the same set of discounts. If not, we need
+ # to list eligibility in the ext_description.
+ my $list_pkgnums = ( $pkgnums_times_discounts != $pkgnums * keys(%total) );
+
+ foreach my $hash (values %total) {
+ my $discount = $hash->{discount};
+ my ($amount, $term_total, $percent, $permonth);
+ my $months = $discount->months;
+ $hash->{months} = $months;
+
+ if ( $discount->percent ) {
+
+ # per discount_Mixin, percent discounts are calculated on the base
+ # recurring fee, not the prorated fee.
+ $percent = $discount->percent;
+ $amount = sprintf('%.2f', 0.01 * $percent * $hash->{base_permonth});
+ # percent discounts apply to setup fee
+ if ( $discount->setup ) {
+ $hash->{setup} *= (1 - 0.01*$percent);
+ }
+
+ }
+ elsif ( $discount->amount > 0 ) {
+
+ # amount discounts are amount * number of packages
+ $amount = $discount->amount * scalar(@{ $hash->{pkgnums} });
+ $percent = sprintf('%.0f', 100 * $amount / $hash->{base_permonth});
+
+ # flat discounts are applied to setup and recur together
+ if ( $discount->setup ) {
+ $hash->{thismonth} += $hash->{setup};
+ $hash->{setup} = 0;
+ }
+
+ }
+
+ $permonth = max( $hash->{base_permonth} - $amount, 0);
+ $term_total = max( $hash->{thismonth} - $amount , 0 ) # this month
+ + $permonth * ($months - 1) # rest of the term
+ + $hash->{setup}; # setup fee
+
+ $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
+ $percent, $months,
+ );
+ $hash->{amount} = $self->mt('[_1] ([_2] per month)',
+ sprintf('%.2f',$term_total), #no money_char to accommodate template quirk
+ $money_char.sprintf('%.2f',$permonth) );
+
+ my @detail;
+ if ( $list_pkgnums ) {
+ push @detail, $self->mt('for item'). ' '.
+ join(', ', map { "#$_" } @{ $hash->{pkgnums} });
+ }
+ if ( !$discount->setup and $hash->{setup} ) {
+ push @detail, $self->mt('excluding setup fees');
+ }
+ $hash->{ext_description} = join ', ', @detail;
+ }
+
+ sort { -( $a->{months} <=> $b->{months} ) } values(%total);
+}
+
=item call_details [ OPTION => VALUE ... ]
Returns an array of CSV strings representing the call details for this invoice
diff --git a/FS/FS/part_pkg/discount_Mixin.pm b/FS/FS/part_pkg/discount_Mixin.pm
index 761d322..3dc3f6e 100644
--- a/FS/FS/part_pkg/discount_Mixin.pm
+++ b/FS/FS/part_pkg/discount_Mixin.pm
@@ -34,10 +34,6 @@ Takes all the arguments of calc_recur. Calculates and returns the amount
by which to reduce the recurring fee; also increments months used on the
discount and generates an invoice detail describing it.
-If the configuration option 'discount-show_available' is enabled, and this
-package is eligible for a prepayment discount but doesn't have one, an
-invoice detail will be generated to describe the available discounts.
-
=cut
sub calc_discount {
@@ -171,46 +167,7 @@ sub calc_discount {
$tot_discount += $amount;
}
- if (!@cust_pkg_discount and $conf->exists('discount-show_available') ) {
- push @$details, $self->available_discounts;
- }
-
sprintf('%.2f', $tot_discount);
}
-=item available_discounts
-
-Returns a list of details decribing the available prepayment discounts
-for this package.
-
-=cut
-
-sub available_discounts {
- my $self = shift;
- return if $self->freq ne '1';
- my @details;
- my $money_char = FS::Conf->new->config('money_char') || '$';
- my @discounts = map { $_->discount } $self->part_pkg_discount;
- # probably the most logical way to arrange these
- foreach my $discount (sort { $a->months <=> $b->months } @discounts) {
- my $months = $discount->months;
- my $amount;
- if ( $discount->amount > 0 ) {
- $amount = $money_char . sprintf('%.2f', $discount->amount);
- }
- elsif ( $discount->percent ) {
- $amount = $discount->percent .'%';
- }
- else { #?
- next;
- }
- push @details, "Prepay $months months for $amount discount."
- # better way to display this?
- # if it's a problem, we'll add discount.invoice_text or something
- # for the customer-visible text line.
- }
- return @details;
-}
-
-
1;
diff --git a/FS/FS/part_pkg_discount.pm b/FS/FS/part_pkg_discount.pm
index 2187e10..06a0fb9 100644
--- a/FS/FS/part_pkg_discount.pm
+++ b/FS/FS/part_pkg_discount.pm
@@ -112,7 +112,7 @@ Returns the discount associated with this part_pkg_discount.
sub discount {
my $self = shift;
- qsearch('discount', { 'discountnum' => $self->discountnum });
+ qsearchs('discount', { 'discountnum' => $self->discountnum });
}
=back
diff --git a/conf/invoice_html b/conf/invoice_html
index 1d53683..567385b 100644
--- a/conf/invoice_html
+++ b/conf/invoice_html
@@ -191,7 +191,7 @@
}
- if ($section->{'description'} || $multisection) {
+ if ($section->{'description'} || $multisection and !$section->{no_subtotal}) {
my $style = 'border-top: 3px solid #000000;'.
'border-bottom: 3px solid #000000;';
$OUT .=
diff --git a/conf/invoice_latex b/conf/invoice_latex
index 37f59d2..772c2eb 100644
--- a/conf/invoice_latex
+++ b/conf/invoice_latex
@@ -272,7 +272,7 @@
$OUT .= '\endfoot';
$OUT .= '\hline';
- if (scalar(@sections) > 1) {
+ if (scalar(@sections) > 1 and !$section->{no_subtotal}) {
if ($section->{total_generator}) {
$OUT .= &{$section->{total_generator}}($section);
} else {