summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2015-01-31 14:44:16 -0800
committerMark Wells <mark@freeside.biz>2015-01-31 14:44:22 -0800
commitf6abf4cd6d8e7a0121124e9394b5f28f5bc4daa5 (patch)
treedb6264c4c2c38a423310d82d4443580607ecc82d
parent3d796bf211374d941bda1116ee27a0543045ea8a (diff)
discounts + quotations, #33099
-rw-r--r--FS/FS/Schema.pm4
-rw-r--r--FS/FS/TemplateItem_Mixin.pm10
-rw-r--r--FS/FS/Template_Mixin.pm162
-rw-r--r--FS/FS/cust_bill_pkg.pm39
-rw-r--r--FS/FS/cust_bill_pkg_discount.pm33
-rw-r--r--FS/FS/quotation.pm21
-rw-r--r--FS/FS/quotation_pkg.pm253
-rw-r--r--FS/FS/quotation_pkg_discount.pm49
-rw-r--r--httemplate/edit/process/quick-cust_pkg.cgi2
9 files changed, 437 insertions, 136 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index b7611c1..d5ed1b7 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1895,6 +1895,8 @@ sub tables_hashref {
'contract_end', @date_type, '', '',
'quantity', 'int', 'NULL', '', '', '',
'waive_setup', 'char', 'NULL', 1, '', '',
+ 'unitsetup', @money_typen, '', '',
+ 'unitrecur', @money_typen, '', '',
],
'primary_key' => 'quotationpkgnum',
'unique' => [],
@@ -1917,6 +1919,8 @@ sub tables_hashref {
'quotationpkgdiscountnum', 'serial', '', '', '', '',
'quotationpkgnum', 'int', '', '', '', '',
'discountnum', 'int', '', '', '', '',
+ 'setup_amount', @money_typen, '', '',
+ 'recur_amount', @money_typen, '', '',
#'end_date', @date_type, '', '',
],
'primary_key' => 'quotationpkgdiscountnum',
diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm
index 6ae3364..27b8f1b 100644
--- a/FS/FS/TemplateItem_Mixin.pm
+++ b/FS/FS/TemplateItem_Mixin.pm
@@ -367,15 +367,17 @@ sub cust_bill_pkg_detail {
}
-=item cust_bill_pkg_discount
+=item pkg_discount
-Returns the list of associated cust_bill_pkg_discount objects.
+Returns the list of associated cust_bill_pkg_discount or
+quotation_pkg_discount objects.
=cut
-sub cust_bill_pkg_discount {
+sub pkg_discount {
my $self = shift;
- qsearch( $self->discount_table, { 'billpkgnum' => $self->billpkgnum } );
+ my $pkey = $self->primary_key;
+ qsearch( $self->discount_table, { $pkey => $self->get($pkey) } );
}
1;
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index 9669ac2..e26592c 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -691,11 +691,12 @@ sub print_generic {
# (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'} = {};
+ # flag telling this invoice to have a first-page summary
+ my $summarypage = '';
if ( $self->custnum && $self->invnum ) {
+ # XXX should be an FS::cust_bill method to set the defaults, instead
+ # of checking the type here
my $last_bill = $self->previous_bill;
if ( $last_bill ) {
@@ -801,13 +802,16 @@ sub print_generic {
$invoice_data{'previous_payments'} = [];
$invoice_data{'previous_credits'} = [];
}
- } # if this is an invoice
- my $summarypage = '';
- if ( $conf->exists('invoice_usesummary', $agentnum) ) {
- $summarypage = 1;
- }
- $invoice_data{'summarypage'} = $summarypage;
+ # info from customer's last invoice before this one, for some
+ # summary formats
+ $invoice_data{'last_bill'} = {};
+
+ if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+ $invoice_data{'summarypage'} = $summarypage = 1;
+ }
+
+ } # if this is an invoice
warn "$me substituting variables in notes, footer, smallfooter\n"
if $DEBUG > 1;
@@ -3093,7 +3097,9 @@ sub _items_cust_bill_pkg {
if $cust_bill_pkg->recur != 0
|| $discount_show_always
|| $cust_bill_pkg->recur_show_zero;
- push @b, {
+ #push @b, {
+ # keep it consistent, please
+ $s = {
'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
'description' => $description,
'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
@@ -3106,7 +3112,8 @@ sub _items_cust_bill_pkg {
};
}
if ( $cust_bill_pkg->recur != 0 ) {
- push @b, {
+ #push @b, {
+ $r = {
'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
@@ -3399,89 +3406,6 @@ sub _items_cust_bill_pkg {
} # recurring or usage with recurring charge
- # decide whether to show active discounts here
- if (
- # case 1: we are showing a single line for the package
- ( !$type )
- # case 2: we are showing a setup line for a package that has
- # no base recurring fee
- or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
- # case 3: we are showing a recur line for a package that has
- # a base recurring fee
- or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
- ) {
-
- # the line item hashref for the line that will show the original
- # price
- # (use the recur or single line for the package, unless we're
- # showing a setup line for a package with no recurring fee)
- my $active_line = $r;
- if ( $type eq 'S' ) {
- $active_line = $s;
- }
-
- my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
- # special case: if there are old "discount details" on this line
- # item, don't show discount line items
- if ( FS::cust_bill_pkg_detail->count(
- "detail LIKE 'Includes discount%' AND billpkgnum = " .
- $cust_bill_pkg->billpkgnum
- ) > 0 ) {
- @discounts = ();
- }
- if ( @discounts ) {
- warn "$me _items_cust_bill_pkg including discounts for ".
- $cust_bill_pkg->billpkgnum."\n"
- if $DEBUG;
- my $discount_amount = sum( map {$_->amount} @discounts );
- # if multiple discounts apply to the same package, how to display
- # them? ext_description lines, apparently
- #
- # # discount amounts are negative
- if ( $d and $cust_bill_pkg->hidden ) {
- $d->{amount} -= $discount_amount;
- } else {
- my @ext;
- $d = {
- _is_discount => 1,
- description => $self->mt('Discount'),
- amount => -1 * $discount_amount,
- ext_description => \@ext,
- };
- foreach my $cust_bill_pkg_discount (@discounts) {
- my $discount = $cust_bill_pkg_discount->cust_pkg_discount->discount;
- my $discount_desc = $discount->description_short;
-
- if ($discount->months) {
-
- # calculate months remaining after this invoice
- my $used = FS::Record->scalar_sql(
- 'SELECT SUM(months) FROM cust_bill_pkg_discount
- JOIN cust_bill_pkg USING (billpkgnum)
- JOIN cust_bill USING (invnum)
- WHERE pkgdiscountnum = ? AND _date <= ?',
- $cust_bill_pkg_discount->pkgdiscountnum,
- $self->_date
- );
- $used ||= 0;
- my $remaining = sprintf('%.2f', $discount->months - $used);
- # append "for X months (Y months remaining)"
- $discount_desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
- $cust_bill_pkg_discount->months,
- $remaining
- );
- } # else it's not time-limited
- push @ext, &{$escape_function}($discount_desc);
- }
- }
-
- # update the active line (before the discount) to show the
- # original price (whether this is a hidden line or not)
- $active_line->{amount} += $discount_amount;
-
- } # if there are any discounts
- } # if this is an appropriate place to show discounts
-
} else { # taxes and fees
warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
@@ -3496,6 +3420,56 @@ sub _items_cust_bill_pkg {
} # if quotation / package line item / other line item
+ # decide whether to show active discounts here
+ if (
+ # case 1: we are showing a single line for the package
+ ( !$type )
+ # case 2: we are showing a setup line for a package that has
+ # no base recurring fee
+ or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
+ # case 3: we are showing a recur line for a package that has
+ # a base recurring fee
+ or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
+ ) {
+
+ my $item_discount = $cust_bill_pkg->_item_discount;
+ if ( $item_discount ) {
+ # $item_discount->{amount} is negative
+
+ if ( $d and $cust_bill_pkg->hidden ) {
+ $d->{amount} += $item_discount->{amount};
+ } else {
+ $d = $item_discount;
+ $_ = &{$escape_function}($_) foreach @{ $d->{ext_description} };
+ }
+
+ # update the active line (before the discount) to show the
+ # original price (whether this is a hidden line or not)
+ #
+ # quotation discounts keep track of setup and recur; invoice
+ # discounts currently don't
+ if ( exists $item_discount->{setup_amount} ) {
+
+ $s->{amount} -= $item_discount->{setup_amount} if $s;
+ $r->{amount} -= $item_discount->{recur_amount} if $r;
+
+ } else {
+
+ # $active_line is the line item hashref for the line that will
+ # show the original price
+ # (use the recur or single line for the package, unless we're
+ # showing a setup line for a package with no recurring fee)
+ my $active_line = $r;
+ if ( $type eq 'S' ) {
+ $active_line = $s;
+ }
+ $active_line->{amount} -= $item_discount->{amount};
+
+ }
+
+ } # if there are any discounts
+ } # if this is an appropriate place to show discounts
+
} # foreach $display
$discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index 56a666e..352ed6a 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -668,6 +668,45 @@ sub units {
$self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
}
+=item _item_discount
+
+If this item has any discounts, returns a hashref in the format used
+by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
+on an invoice. This will contain the keys 'description', 'amount',
+'ext_description' (an arrayref of text lines describing the discounts),
+and '_is_discount' (a flag).
+
+The value for 'amount' will be negative, and will be scaled for the package
+quantity.
+
+=cut
+
+sub _item_discount {
+ my $self = shift;
+ my @pkg_discounts = $self->pkg_discount;
+ return if @pkg_discounts == 0;
+ # special case: if there are old "discount details" on this line item, don't
+ # show discount line items
+ if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
+ return;
+ }
+
+ my @ext;
+ my $d = {
+ _is_discount => 1,
+ description => $self->mt('Discount'),
+ amount => 0,
+ ext_description => \@ext,
+ # maybe should show quantity/unit discount?
+ };
+ foreach my $pkg_discount (@pkg_discounts) {
+ push @ext, $pkg_discount->description;
+ $d->{amount} -= $pkg_discount->amount;
+ }
+ $d->{amount} *= $self->quantity || 1;
+
+ return $d;
+}
=item set_display OPTION => VALUE ...
diff --git a/FS/FS/cust_bill_pkg_discount.pm b/FS/FS/cust_bill_pkg_discount.pm
index 534a067..9e64d20 100644
--- a/FS/FS/cust_bill_pkg_discount.pm
+++ b/FS/FS/cust_bill_pkg_discount.pm
@@ -126,6 +126,39 @@ Returns the associated line item (see L<FS::cust_bill_pkg>).
Returns the associated customer discount (see L<FS::cust_pkg_discount>).
+=item description
+
+Returns a string describing the discount (for use on an invoice).
+
+=cut
+
+sub description {
+ my $self = shift;
+ my $discount = $self->cust_pkg_discount->discount;
+ my $desc = $discount->description_short;
+ $desc .= $self->mt(' each') if $self->cust_bill_pkg->quantity > 1;
+
+ if ($discount->months) {
+ # calculate months remaining on this cust_pkg_discount after this invoice
+ my $date = $self->cust_bill_pkg->cust_bill->_date;
+ my $used = FS::Record->scalar_sql(
+ 'SELECT SUM(months) FROM cust_bill_pkg_discount
+ JOIN cust_bill_pkg USING (billpkgnum)
+ JOIN cust_bill USING (invnum)
+ WHERE pkgdiscountnum = ? AND _date <= ?',
+ $self->pkgdiscountnum,
+ $date
+ );
+ $used ||= 0;
+ my $remaining = sprintf('%.2f', $discount->months - $used);
+ $desc .= $self->mt(' for [quant,_1,month] ([quant,_2,month] remaining)',
+ $self->months,
+ $remaining
+ );
+ }
+ return $desc;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm
index 5c94150..38e7318 100644
--- a/FS/FS/quotation.pm
+++ b/FS/FS/quotation.pm
@@ -631,6 +631,27 @@ sub search_sql_where {
}
+=item _items_pkg
+
+Return line item hashes for each package on this quotation. Differs from the
+base L<FS::Template_Mixin> version in that it recalculates each quoted package
+first, and doesn't implement the "condensed" option.
+
+=cut
+
+sub _items_pkg {
+ my ($self, %options) = @_;
+ my @quotation_pkg = $self->quotation_pkg;
+ foreach (@quotation_pkg) {
+ my $error = $_->estimate;
+ die "error calculating estimate for pkgpart " . $_->pkgpart.": $error\n"
+ if $error;
+ }
+
+ # run it through the Template_Mixin engine
+ return $self->_items_cust_bill_pkg(\@quotation_pkg, %options);
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/quotation_pkg.pm b/FS/FS/quotation_pkg.pm
index 33c761e..3813fb2 100644
--- a/FS/FS/quotation_pkg.pm
+++ b/FS/FS/quotation_pkg.pm
@@ -2,9 +2,10 @@ package FS::quotation_pkg;
use base qw( FS::TemplateItem_Mixin FS::Record );
use strict;
-use FS::Record qw( qsearchs ); #qsearch
+use FS::Record qw( qsearchs dbh ); #qsearch
use FS::part_pkg;
use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
+use List::Util qw(sum);
=head1 NAME
@@ -39,19 +40,19 @@ primary key
=item pkgpart
-pkgpart
+pkgpart (L<FS::part_pkg>) of the package
=item locationnum
-locationnum
+locationnum (L<FS::cust_location>) where the package will be in service
=item start_date
-start_date
+expected start date for the package, as a timestamp
=item contract_end
-contract_end
+contract end date
=item quantity
@@ -59,8 +60,15 @@ quantity
=item waive_setup
-waive_setup
+'Y' to waive the setup fee
+=item unitsetup
+
+The amount per package that will be charged in setup/one-time fees.
+
+=item unitrecur
+
+The amount per package that will be charged per billing cycle.
=back
@@ -93,10 +101,69 @@ sub discount_table { 'quotation_pkg_discount'; }
Adds this record to the database. If there is an error, returns the error,
otherwise returns false.
+=cut
+
+sub insert {
+ my ($self, %options) = @_;
+
+ my $dbh = dbh;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ my $error = $self->SUPER::insert;
+
+ if ( !$error and $self->discountnum ) {
+ $error = $self->insert_discount;
+ $error .= ' (setting discount)' if $error;
+ }
+
+ # update $self and any discounts with their amounts
+ if ( !$error ) {
+ $error = $self->estimate;
+ $error .= ' (calculating charges)' if $error;
+ }
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ } else {
+ $dbh->commit if $oldAutoCommit;
+ return '';
+ }
+}
+
=item delete
Delete this record from the database.
+=cut
+
+sub delete {
+ my $self = shift;
+
+ my $dbh = dbh;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ foreach ($self->quotation_pkg_discount) {
+ my $error = $_->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error . ' (deleting discount)';
+ }
+ }
+
+ my $error = $self->SUPER::delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ } else {
+ $dbh->commit if $oldAutoCommit;
+ return '';
+ }
+
+}
+
=item replace OLD_RECORD
Replaces the OLD_RECORD with this one in the database. If there is an error,
@@ -121,8 +188,11 @@ sub check {
|| $self->ut_numbern('start_date')
|| $self->ut_numbern('contract_end')
|| $self->ut_numbern('quantity')
+ || $self->ut_moneyn('unitsetup')
+ || $self->ut_moneyn('unitrecur')
|| $self->ut_enum('waive_setup', [ '', 'Y'] )
;
+
return $error if $error;
$self->SUPER::check;
@@ -140,48 +210,159 @@ sub desc {
$self->part_pkg->pkg;
}
-sub setup {
+=item estimate
+
+Update the quotation_pkg record with the estimated setup and recurring
+charges for the package. Returns nothing on success, or an error message
+on failure.
+
+=cut
+
+sub estimate {
my $self = shift;
- return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
my $part_pkg = $self->part_pkg;
- #my $setup = $part_pkg->can('base_setup') ? $part_pkg->base_setup
- # : $part_pkg->option('setup_fee');
- my $setup = $part_pkg->option('setup_fee');
- #XXX discounts
- $setup *= $self->quantity if $self->quantity;
- sprintf('%.2f', $setup);
+ my $quantity = $self->quantity || 1;
+ my ($unitsetup, $unitrecur);
+ # calculate base fees
+ if ( $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'} ) {
+ $unitsetup = '0.00';
+ } else {
+ $unitsetup = $part_pkg->base_setup;
+ }
+ if ( $self->{'_NO_RECUR_KLUDGE'} ) {
+ $unitrecur = '0.00';
+ } else {
+ $unitrecur = $part_pkg->base_recur;
+ }
+
+ #XXX add-on packages
+
+ $self->set('unitsetup', $unitsetup);
+ $self->set('unitrecur', $unitrecur);
+ my $error = $self->replace;
+ return $error if $error;
+
+ # semi-duplicates calc_discount
+ my $setup_discount = 0;
+ my $recur_discount = 0;
+
+ my %setup_discounts; # quotationpkgdiscountnum => amount
+ my %recur_discounts; # quotationpkgdiscountnum => amount
+
+ # XXX the order of applying discounts is ill-defined, which matters
+ # if there are percentage and amount discounts on the same package.
+ foreach my $pkg_discount ($self->quotation_pkg_discount) {
+
+ my $discount = $pkg_discount->discount;
+ my $this_setup_discount = 0;
+ my $this_recur_discount = 0;
+
+ if ( $discount->percent > 0 ) {
+
+ if ( $discount->setup ) {
+ $this_setup_discount = ($discount->percent * $unitsetup / 100);
+ }
+ $this_recur_discount = ($discount->percent * $unitrecur / 100);
+
+ } elsif ( $discount->amount > 0 ) {
+
+ my $discount_left = $discount->amount;
+ if ( $discount->setup ) {
+ if ( $discount_left > $unitsetup - $setup_discount ) {
+ # then discount the setup to zero
+ $discount_left -= $unitsetup - $setup_discount;
+ $this_setup_discount = $unitsetup - $setup_discount;
+ } else {
+ # not enough discount to fully cover the setup
+ $this_setup_discount = $discount_left;
+ $discount_left = 0;
+ }
+ }
+ # same logic for recur
+ if ( $discount_left > $unitrecur - $recur_discount ) {
+ $this_recur_discount = $unitrecur - $recur_discount;
+ } else {
+ $this_recur_discount = $discount_left;
+ }
+
+ }
+
+ # increment the total discountage
+ $setup_discount += $this_setup_discount;
+ $recur_discount += $this_recur_discount;
+ # and update the pkg_discount object
+ $pkg_discount->set('setup_amount', sprintf('%.2f', $setup_discount));
+ $pkg_discount->set('recur_amount', sprintf('%.2f', $recur_discount));
+ my $error = $pkg_discount->replace;
+ return $error if $error;
+ }
+ '';
}
-sub recur {
+=item insert_discount
+
+Associates this package with a discount (see L<FS::cust_pkg_discount>,
+possibly inserting a new discount on the fly (see L<FS::discount>). Properties
+of the discount will be taken from this object.
+
+=cut
+
+sub insert_discount {
+ #my ($self, %options) = @_;
my $self = shift;
- return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
- my $part_pkg = $self->part_pkg;
- my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur($self)
- : $part_pkg->option('recur_fee');
- #XXX discounts
- $recur *= $self->quantity if $self->quantity;
- sprintf('%.2f', $recur);
+
+ my $cust_pkg_discount = FS::quotation_pkg_discount->new( {
+ 'quotationpkgnum' => $self->quotationpkgnum,
+ 'discountnum' => $self->discountnum,
+ #for the create a new discount case
+ '_type' => $self->discountnum__type,
+ 'amount' => $self->discountnum_amount,
+ 'percent' => $self->discountnum_percent,
+ 'months' => $self->discountnum_months,
+ 'setup' => $self->discountnum_setup,
+ } );
+
+ $cust_pkg_discount->insert;
}
-sub unitsetup {
+sub _item_discount {
my $self = shift;
- return '0.00' if $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'};
- my $part_pkg = $self->part_pkg;
- my $setup = $part_pkg->option('setup_fee');
+ my @pkg_discounts = $self->pkg_discount;
+ return if @pkg_discounts == 0;
+
+ my @ext;
+ my $d = {
+ _is_discount => 1,
+ description => $self->mt('Discount'),
+ setup_amount => 0,
+ recur_amount => 0,
+ amount => 0,
+ ext_description => \@ext,
+ # maybe should show quantity/unit discount?
+ };
+ foreach my $pkg_discount (@pkg_discounts) {
+ push @ext, $pkg_discount->description;
+ $d->{setup_amount} -= $pkg_discount->setup_amount;
+ $d->{recur_amount} -= $pkg_discount->recur_amount;
+ }
+ $d->{setup_amount} *= $self->quantity || 1;
+ $d->{recur_amount} *= $self->quantity || 1;
+ $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
+
+ return $d;
+}
- #XXX discounts
- sprintf('%.2f', $setup);
+sub setup {
+ my $self = shift;
+ ($self->unitsetup - sum(map { $_->setup_amount } $self->pkg_discount))
+ * ($self->quantity || 1);
}
-sub unitrecur {
+sub recur {
my $self = shift;
- return '0.00' if $self->{'_NO_RECUR_KLUDGE'};
- my $part_pkg = $self->part_pkg;
- my $recur = $part_pkg->can('base_recur') ? $part_pkg->base_recur
- : $part_pkg->option('recur_fee');
- #XXX discounts
- sprintf('%.2f', $recur);
+ ($self->unitrecur - sum(map { $_->recur_amount } $self->pkg_discount))
+ * ($self->quantity || 1);
}
=item part_pkg_currency_option OPTIONNAME
@@ -273,6 +454,8 @@ sub prospect_main {
=head1 BUGS
+Doesn't support taxes, fees, or add-on packages.
+
=head1 SEE ALSO
L<FS::Record>, schema.html from the base documentation.
diff --git a/FS/FS/quotation_pkg_discount.pm b/FS/FS/quotation_pkg_discount.pm
index 19930ac..633308c 100644
--- a/FS/FS/quotation_pkg_discount.pm
+++ b/FS/FS/quotation_pkg_discount.pm
@@ -1,5 +1,6 @@
package FS::quotation_pkg_discount;
use base qw( FS::Record );
+use FS::Maketext 'mt'; # XXX not really correct
use strict;
@@ -36,12 +37,21 @@ primary key
=item quotationpkgnum
-quotationpkgnum
+quotationpkgnum of the L<FS::quotation_pkg> record that this discount is
+for.
=item discountnum
-discountnum
+discountnum (L<FS::discount>)
+=item setup_amount
+
+Amount that will be discounted from setup fees, per package quantity.
+
+=item recur_amount
+
+Amount that will be discounted from recurring fees in the first billing
+cycle, per package quantity.
=back
@@ -107,6 +117,8 @@ sub check {
$self->ut_numbern('quotationpkgdiscountnum')
|| $self->ut_foreign_key('quotationpkgnum', 'quotation_pkg', 'quotationpkgnum' )
|| $self->ut_foreign_key('discountnum', 'discount', 'discountnum' )
+ || $self->ut_moneyn('setup_amount')
+ || $self->ut_moneyn('recur_amount')
;
return $error if $error;
@@ -115,6 +127,39 @@ sub check {
=back
+=item amount
+
+Returns the total amount of this discount (setup + recur), for compatibility
+with L<FS::cust_bill_pkg_discount>.
+
+=cut
+
+sub amount {
+ my $self = shift;
+ return $self->get('setup_amount') + $self->get('recur_amount');
+}
+
+=item description
+
+Returns a string describing the discount (for use on the quotation).
+
+=cut
+
+sub description {
+ my $self = shift;
+ my $discount = $self->discount;
+ my $desc = $discount->description_short;
+ # XXX localize to prospect language, once prospects get languages
+ $desc .= mt(' each') if $self->quotation_pkg->quantity > 1;
+
+ if ($discount->months) {
+ # unlike cust_bill_pkg_discount, there are no "months remaining"; it
+ # hasn't started yet.
+ $desc .= mt(' (for [quant,_1,month])', $discount->months);
+ }
+ return $desc;
+}
+
=head1 BUGS
=head1 SEE ALSO
diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi
index f1d8c26..34f5d12 100644
--- a/httemplate/edit/process/quick-cust_pkg.cgi
+++ b/httemplate/edit/process/quick-cust_pkg.cgi
@@ -159,7 +159,7 @@ if ( $quotationnum ) {
$quotation_pkg->prospectnum($prospect_main->prospectnum) if $prospect_main;
#XXX handle new location
- $error = $quotation_pkg->insert;
+ $error = $quotation_pkg->insert || $quotation_pkg->estimate;
} else {