From 5250c44bd7f282c7d782bf0e8349af12376929df Mon Sep 17 00:00:00 2001 From: jeff Date: Wed, 22 Sep 2010 19:16:20 +0000 Subject: [PATCH] prepayment discounts rt#5318 --- FS/FS/ClientAPI/MyAccount.pm | 18 +- FS/FS/Conf.pm | 20 ++ FS/FS/Mason.pm | 1 + FS/FS/Schema.pm | 12 ++ FS/FS/cust_bill.pm | 39 ++++ FS/FS/cust_credit_bill_pkg.pm | 19 +- FS/FS/cust_main/Billing.pm | 238 +++++++++++++++++++-- FS/FS/cust_main/Billing_Realtime.pm | 3 + FS/FS/cust_main_county.pm | 5 +- FS/FS/cust_pay.pm | 53 +++++ FS/FS/cust_pkg.pm | 12 ++ FS/FS/discount.pm | 11 + FS/FS/part_pkg.pm | 13 ++ FS/FS/part_pkg/flat.pm | 30 ++- FS/FS/part_pkg_discount.pm | 129 +++++++++++ FS/MANIFEST | 2 + FS/t/part_pkg_discount.t | 5 + .../FS-SelfService/cgi/discount_term.html | 17 ++ .../FS-SelfService/cgi/make_ach_payment.html | 1 + .../FS-SelfService/cgi/make_payment.html | 4 +- fs_selfservice/FS-SelfService/cgi/myaccount.html | 10 +- fs_selfservice/FS-SelfService/cgi/selfservice.cgi | 24 ++- httemplate/browse/part_pkg.cgi | 25 +++ httemplate/edit/cust_pay.cgi | 6 + httemplate/edit/part_pkg.cgi | 55 +++++ httemplate/edit/process/cust_pay.cgi | 2 +- httemplate/edit/process/part_pkg.cgi | 6 + httemplate/elements/customer-table.html | 13 +- httemplate/elements/select-discount_term.html | 32 +++ httemplate/elements/tr-select-discount_term.html | 25 +++ httemplate/misc/batch-cust_pay.html | 98 ++++++++- httemplate/misc/payment.cgi | 5 + httemplate/misc/process/batch-cust_pay.cgi | 13 +- httemplate/misc/process/payment.cgi | 26 ++- .../misc/xmlhttp-cust_main-discount_terms.cgi | 24 +++ httemplate/view/cust_main/packages/package.html | 1 + 36 files changed, 933 insertions(+), 64 deletions(-) create mode 100644 FS/FS/part_pkg_discount.pm create mode 100644 FS/t/part_pkg_discount.t create mode 100644 fs_selfservice/FS-SelfService/cgi/discount_term.html create mode 100644 httemplate/elements/select-discount_term.html create mode 100644 httemplate/elements/tr-select-discount_term.html create mode 100644 httemplate/misc/xmlhttp-cust_main-discount_terms.cgi diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 5ecb71b75..dbcef7d4c 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -357,6 +357,11 @@ sub customer_info { $return{support_services} = \@support_services; } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + } elsif ( $session->{'svcnum'} ) { #no customer record my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } ) @@ -459,10 +464,10 @@ sub payment_info { #generic ## + my $conf = new FS::Conf; use vars qw($payment_info); #cache for performance unless ( $payment_info ) { - my $conf = new FS::Conf; my %states = map { $_->state => 1 } qsearch('cust_main_county', { 'country' => $conf->config('countrydefault') || 'US' @@ -555,6 +560,11 @@ sub payment_info { } + if ( $conf->config('prepayment_discounts-credit_type') ) { + #need to eval? + $return{discount_terms_hash} = { $cust_main->discount_terms_hash }; + } + #doubleclick protection my $_date = time; $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32; @@ -586,6 +596,10 @@ sub process_payment { my $amount = $1; return { error => 'Amount must be greater than 0' } unless $amount > 0; + $p->{'discount_term'} =~ /^\s*(\d+)\s*$/ + or return { 'error' => gettext('illegal_discount_term'). ': '. $p->{'discount_term'} }; + my $discount_term = $1; + $p->{'payname'} =~ /^([\w \,\.\-\']+)$/ or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} }; my $payname = $1; @@ -664,6 +678,7 @@ sub process_payment { 'paybatch' => $paybatch, #this doesn't actually do anything 'paycvv' => $paycvv, 'pkgnum' => $session->{'pkgnum'}, + 'discount_term' => $discount_term, map { $_ => $p->{$_} } @{ $payby2fields{$payby} } ); return { 'error' => $error } if $error; @@ -1728,6 +1743,7 @@ sub _custoragent_session_custnum { $custnum = $p->{'custnum'}; } else { + $context = 'error'; return ( 'error' => "Can't resume session" ); #better error message } diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 9b21a5ac6..628462ef4 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3162,6 +3162,26 @@ and customer address. Include units.', }, { + 'key' => 'prepayment_discounts-credit_type', + 'section' => 'billing', + 'description' => 'Enables the offering of prepayment discounts and establishes the credit reason type.', + 'type' => 'select-sub', + 'options_sub' => sub { require FS::Record; + require FS::reason_type; + map { $_->typenum => $_->type } + FS::Record::qsearch('reason_type', { class=>'R' } ); + }, + 'option_sub' => sub { require FS::Record; + require FS::reason_type; + my $reason_type = FS::Record::qsearchs( + 'reason_type', { 'typenum' => shift } + ); + $reason_type ? $reason_type->type : ''; + }, + + }, + + { 'key' => 'cust_main-agent_custid-format', 'section' => '', 'description' => 'Enables searching of various formatted values in cust_main.agent_custid', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 5c57cbefc..2282bc58c 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -254,6 +254,7 @@ if ( -e $addl_handler_use_file ) { use FS::msg_template; use FS::part_tag; use FS::acct_snarf; + use FS::part_pkg_discount; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 8403ea2d6..57ef18eef 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -570,6 +570,7 @@ sub tables_hashref { 'itemdesc', 'varchar', 'NULL', $char_d, '', '', 'itemcomment', 'varchar', 'NULL', $char_d, '', '', 'section', 'varchar', 'NULL', $char_d, '', '', + 'freq', 'varchar', 'NULL', $char_d, '', '', 'quantity', 'int', 'NULL', '', '', '', 'unitsetup', @money_typen, '', '', 'unitrecur', @money_typen, '', '', @@ -1512,6 +1513,17 @@ sub tables_hashref { # XXX somewhat borked unique: we don't really want a hidden and unhidden # it turns out we'd prefer to use svc, bill, and invisibill (or something) + 'part_pkg_discount' => { + 'columns' => [ + 'pkgdiscountnum', 'serial', '', '', '', '', + 'pkgpart', 'int', '', '', '', '', + 'discountnum', 'int', '', '', '', '', + ], + 'primary_key' => 'pkgdiscountnum', + 'unique' => [ [ 'pkgpart', 'discountnum' ] ], + 'index' => [], + }, + 'part_pkg_taxclass' => { 'columns' => [ 'taxclassnum', 'serial', '', '', '', '', diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index bb392e83d..d364ac527 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -162,6 +162,45 @@ sub cust_unlinked_msg { Adds this invoice to the database ("Posts" the invoice). If there is an error, returns the error, otherwise returns false. +=cut + +sub insert { + my $self = shift; + warn "$me insert called\n" if $DEBUG; + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $error = $self->SUPER::insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + if ( $self->get('cust_bill_pkg') ) { + foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) { + $cust_bill_pkg->invnum($self->invnum); + my $error = $cust_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't create invoice line item: $error"; + } + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + =item delete This method now works but you probably shouldn't use it. Instead, apply a diff --git a/FS/FS/cust_credit_bill_pkg.pm b/FS/FS/cust_credit_bill_pkg.pm index 019a1a874..64f1f297e 100644 --- a/FS/FS/cust_credit_bill_pkg.pm +++ b/FS/FS/cust_credit_bill_pkg.pm @@ -106,7 +106,10 @@ sub insert { my $payable = $self->cust_bill_pkg->payable($self->setuprecur); my $taxable = $self->_is_taxable ? $payable : 0; my $part_pkg = $self->cust_bill_pkg->part_pkg; - my $freq = $part_pkg ? $part_pkg->freq || 1 : 1;# assume unchanged + my $freq = $self->cust_bill_pkg->freq; + unless ($freq) { + $freq = $part_pkg ? ($part_pkg->freq || 1) : 1;#fallback.. assumes unchanged + } my $taxable_per_month = sprintf("%.2f", $taxable / $freq ); my $credit_per_month = sprintf("%.2f", $self->amount / $freq ); #pennies? @@ -334,13 +337,13 @@ sub cust_bill_pkg_tax_Xlocation { B field is a kludge to compensate for cust_bill_pkg having separate setup and recur fields. It should be removed once that's fixed. -B method assumes that the frequency of the package associated with the -associated line item remains unchanged during the lifetime of the system. -It may get the tax exemption adjustments wrong if package definitions change -frequency. The presense of delete methods in FS::cust_main_county and -FS::tax_rate makes crediting of old "texas tax" unreliable in the presense of -changing taxes. Explicit tax credit requests? Carry 'taxable' onto line -items? +B method used to assume that the frequency of the package associated +with the associated line item remained unchanged during the lifetime of the +system. That is still used as a fallback. It may get the tax exemption +adjustments wrong if package definitions change frequency. The presense of +delete methods in FS::cust_main_county and FS::tax_rate makes crediting of +old "texas tax" unreliable in the presense of changing taxes. Explicit tax +credit requests? Carry 'taxable' onto line items? =head1 SEE ALSO diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 09c0b64d3..3ebc87da3 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -243,6 +243,16 @@ Options are passed as name-value pairs. Currently available options are: If set true, re-charges setup fees. +=item recurring_only + +If set true then only bill recurring charges, not setup, usage, one time +charges, etc. + +=item freq_override + +If set, then override the normal frequency and look for a part_pkg_discount +to take at that frequency. + =item time Bills the customer as if it were that time. Specified as a UNIX timestamp; see L). Also see L and L for conversion functions. For example: @@ -272,6 +282,18 @@ typically might mean not charging the normal recurring fee but only usage fees since the last billing. Setup charges may be charged. Not all package plans support this feature (they tend to charge 0). +=item no_usage_reset + +Prevent the resetting of usage limits during this call. + +=item no_commit + +Do not save the generated bill in the database. Useful with return_bill + +=item return_bill + +A list reference on which the generated bill(s) will be returned. + =item invoice_terms Optional terms to be printed on this invoice. Otherwise, customer-specific @@ -320,9 +342,10 @@ sub bill { 'time' => $invoice_time, 'check_freq' => $options{'check_freq'}, 'stage' => 'pre-bill', - ); + ) + unless $options{no_commit}; if ( $error ) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return $error; } @@ -387,7 +410,7 @@ sub bill { 'options' => \%options, ); if ($error) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return $error; } @@ -415,7 +438,7 @@ sub bill { my $postal_pkg = $self->charge_postal_fee(); if ( $postal_pkg && !ref( $postal_pkg ) ) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return "can't charge postal invoice fee for customer ". $self->custnum. ": $postal_pkg"; @@ -444,7 +467,7 @@ sub bill { 'options' => \%postal_options, ); if ($error) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return $error; } } @@ -460,7 +483,7 @@ sub bill { $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time); unless ( ref( $listref_or_error ) ) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return $listref_or_error; } @@ -511,6 +534,7 @@ sub bill { #my $balance_adjustments = # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + warn "creating the new invoice\n" if $DEBUG; #create the new invoice my $cust_bill = new FS::cust_bill ( { 'custnum' => $self->custnum, @@ -519,35 +543,29 @@ sub bill { 'billing_balance' => $balance, 'previous_balance' => $previous_balance, 'invoice_terms' => $options{'invoice_terms'}, + 'cust_bill_pkg' => \@cust_bill_pkg, } ); - $error = $cust_bill->insert; + $error = $cust_bill->insert unless $options{no_commit}; if ( $error ) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return "can't create invoice for customer #". $self->custnum. ": $error"; } - - foreach my $cust_bill_pkg ( @cust_bill_pkg ) { - $cust_bill_pkg->invnum($cust_bill->invnum); - my $error = $cust_bill_pkg->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "can't create invoice line item: $error"; - } - } + push @{$options{return_bill}}, $cust_bill if $options{return_bill}; } #foreach my $pass ( keys %cust_bill_pkg ) foreach my $hook ( @precommit_hooks ) { eval { &{$hook}; #($self) ? - }; + } unless $options{no_commit}; if ( $@ ) { - $dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit && !$options{no_commit}; return "$@ running precommit hook $hook\n"; } } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; + $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit}; + ''; #no error } @@ -792,6 +810,7 @@ sub _make_lines { ) ) ) + and !$options{recurring_only} ) { @@ -836,7 +855,7 @@ sub _make_lines { # XXX should this be a package event? probably. events are called # at collection time at the moment, though... $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG) - if $part_pkg->can('reset_usage'); + if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'}; #don't want to reset usage just cause we want a line item?? #&& $part_pkg->pkgpart == $real_pkgpart; @@ -857,16 +876,21 @@ sub _make_lines { 'increment_next_bill' => $increment_next_bill, 'discounts' => \@discounts, 'real_pkgpart' => $real_pkgpart, + 'freq_override' => $options{freq_override} || '', ); my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur'; + + # There may be some part_pkg for which this is wrong. Only those + # which can_discount are supported. + $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) }; return "$@ running $method for $cust_pkg\n" if ( $@ ); if ( $increment_next_bill ) { - my $next_bill = $part_pkg->add_freq($sdate); + my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); return "unparsable frequency: ". $part_pkg->freq if $next_bill == -1; @@ -902,7 +926,8 @@ sub _make_lines { my $error = $cust_pkg->replace( $old_cust_pkg, 'options' => { $cust_pkg->options }, - ); + ) + unless $options{no_commit}; return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error" if $error; #just in case } @@ -941,6 +966,7 @@ sub _make_lines { 'details' => \@details, 'discounts' => \@discounts, 'hidden' => $part_pkg->hidden, + 'freq' => $part_pkg->freq, }; if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) { @@ -1433,6 +1459,7 @@ set true to surpress email card/ACH decline notices. Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries) +=back =cut # =item payby @@ -1572,6 +1599,171 @@ Explicitly pass the objects to be tested (typically used with eventtable). Set to true to return the objects, but not actually insert them into the database. +=item discount_terms + +Returns a list of lengths for term discounts + +=cut + +sub _discount_pkgs_and_bill { +my $self = shift; + + my @cust_bill = $self->cust_bill; + my $cust_bill = pop @cust_bill; + return () unless $cust_bill && $cust_bill->owed; + + my @where = (); + push @where, "cust_bill_pkg.invnum = ". $cust_bill->invnum; + push @where, "cust_bill_pkg.pkgpart_override IS NULL"; + push @where, "part_pkg.freq = 1"; + push @where, "(cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0)"; + push @where, "(cust_pkg.susp IS NULL OR cust_pkg.susp = 0)"; + push @where, "0<(SELECT count(*) FROM part_pkg_discount + WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart)"; + push @where, + "0=(SELECT count(*) FROM cust_bill_pkg_discount + WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum)"; + + my $extra_sql = 'WHERE '. join(' AND ', @where); + + my @cust_pkg = + qsearch({ + 'table' => 'cust_pkg', + 'select' => "DISTINCT cust_pkg.*", + 'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '. + 'JOIN part_pkg USING(pkgpart)', + 'hashref' => {}, + 'extra_sql' => $extra_sql, + }); + + ($cust_bill, @cust_pkg); +} + +sub _discountable_pkgs_at_term { + my ($term, @pkgs) = @_; + my $part_pkg = new FS::part_pkg { freq => $term - 1 }; + grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) && + ( !$_->expire || $_->expire > $part_pkg->add_freq($_->bill) ) + } + @pkgs; +} + +=item discount_terms + +Returns a list of lengths for term discounts + +=cut + +sub discount_terms { +my $self = shift; + + my %terms = (); + + my @discount_pkgs = $self->_discount_pkgs_and_bill; + shift @discount_pkgs; #discard bill; + + map { $terms{$_->months} = 1 } + grep { $_->months && $_->months > 1 } + map { $_->discount } + map { $_->part_pkg->part_pkg_discount } + @discount_pkgs; + + return sort { $a <=> $b } keys %terms; + +} + +=back + +=item discount_term_values MONTHS + +Returns a list with credit, dollar amount saved, and total bill acheived +by prepaying the most recent invoice for MONTHS. + +=cut + +sub discount_term_values { + my $self = shift; + my $term = shift; + warn "$me discount_term_values called with $term\n" if $DEBUG; + + my %result = (); + + my @packages = $self->_discount_pkgs_and_bill; + my $cust_bill = shift(@packages); + @packages = _discountable_pkgs_at_term( $term, @packages ); + return () unless scalar(@packages); + + $_->bill($_->last_bill) foreach @packages; + my @final = map { new FS::cust_pkg { $_->hash } } @packages; + + my %options = ( + 'recurring_only' => 1, + 'no_usage_reset' => 1, + 'no_commit' => 1, + ); + + my %params = ( + 'return_bill' => [], + 'pkg_list' => \@packages, + 'time' => $cust_bill->_date, + ); + + my $error = $self->bill(%options, %params); + die $error if $error; # XXX think about this a bit more + + my $credit = 0; + $credit += $_->charged foreach @{$params{return_bill}}; + $credit = sprintf('%.2f', $credit); + warn "$me discount_term_values $term credit: $credit\n" if $DEBUG; + + %params = ( + 'return_bill' => [], + 'pkg_list' => \@packages, + 'time' => $packages[0]->part_pkg->add_freq($cust_bill->_date) + ); + + $error = $self->bill(%options, %params); + die $error if $error; # XXX think about this a bit more + + my $next = 0; + $next += $_->charged foreach @{$params{return_bill}}; + warn "$me discount_term_values $term next: $next\n" if $DEBUG; + + %params = ( + 'return_bill' => [], + 'pkg_list' => \@final, + 'time' => $cust_bill->_date, + 'freq_override' => $term, + ); + + $error = $self->bill(%options, %params); + die $error if $error; # XXX think about this a bit more + + my $final = $self->balance - $credit; + $final += $_->charged foreach @{$params{return_bill}}; + $final = sprintf('%.2f', $final); + warn "$me discount_term_values $term final: $final\n" if $DEBUG; + + my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final); + + ( $credit, $savings, $final ); + +} + +sub discount_terms_hash { + my $self = shift; + + my %result = (); + my @terms = $self->discount_terms; + foreach my $term (@terms) { + my @result = $self->discount_term_values($term); + $result{$term} = [ @result ] if scalar(@result); + } + + return %result; + +} + =back =cut diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 4159d04c3..4a9d2c685 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -138,6 +138,8 @@ I is a session identifier associated with this payment. I allows payment capture to unlock export jobs +I attempts to take a discount by prepaying for discount_term + (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too) =cut @@ -746,6 +748,7 @@ sub _realtime_bop_result { 'paybatch' => $paybatch, 'paydate' => $cust_pay_pending->paydate, 'pkgnum' => $cust_pay_pending->pkgnum, + 'discount_term' => $options{'discount_term'}, } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now $cust_pay->payunique( $options{payunique} ) diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index ab1ac1e33..e84fa98f9 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -256,7 +256,10 @@ sub taxline { my ($mon,$year) = (localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5]; $mon++; - my $freq = $part_pkg->freq || 1; + my $freq = $cust_bill_pkg->freq; + unless ($freq) { + $freq = $part_pkg->freq || 1; # less trustworthy fallback + } if ( $freq !~ /(\d+)$/ ) { $dbh->rollback if $oldAutoCommit; return "daily/weekly package definitions not (yet?)". diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 9985f59c7..e0c99f898 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -141,6 +141,10 @@ For backwards-compatibility and convenience, if the additional field invnum is defined, an FS::cust_bill_pay record for the full amount of the payment will be created. In this case, custnum is optional. +If the additional field discount_term is defined then a prepayment discount +is taken for that length of time. It is an error for the customer to owe +after this payment is made. + A hash of optional arguments may be passed. Currently "manual" is supported. If true, a payment receipt is sent instead of a statement when 'payment_receipt_email' configuration option is set. @@ -183,6 +187,51 @@ sub insert { return "error inserting cust_pay: $error"; } + if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) { + if ( my $months = $self->discount_term ) { + #hmmm... error handling + my ($credit, $savings, $total) = + $cust_main->discount_term_values($months); + my $cust_credit = new FS::cust_credit { + 'custnum' => $self->custnum, + 'amount' => $credit, + 'reason' => 'customer chose to prepay for discount', + }; + $error = $cust_credit->insert('reason_type' => $credit_type); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error inserting cust_pay: $error"; + } + my @pkgs = $cust_main->_discount_pkgs_and_bill; + my $cust_bill = shift(@pkgs); + @pkgs = &FS::cust_main::Billing::_discountable_pkgs_at_term($months, @pkgs); + $_->bill($_->last_bill) foreach @pkgs; + $error = $cust_main->bill( + 'recurring_only' => 1, + 'time' => $cust_bill->invoice_date, + 'no_usage_reset' => 1, + 'pkg_list' => \@pkgs, + 'freq_override' => $months, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error inserting cust_pay: $error"; + } + $error = $cust_main->apply_payments_and_credits; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error inserting cust_pay: $error"; + } + my $new_balance = $cust_main->balance; + if ($new_balance > 0) { + $dbh->rollback if $oldAutoCommit; + return "balance after prepay discount attempt: $new_balance"; + } + + } + + } + if ( $self->invnum ) { my $cust_bill_pay = new FS::cust_bill_pay { 'invnum' => $self->invnum, @@ -388,6 +437,7 @@ sub check { || $self->ut_enum('closed', [ '', 'Y' ]) || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') || $self->payinfo_check() + || $self->ut_numbern('discount_term') ; return $error if $error; @@ -399,6 +449,9 @@ sub check { $self->_date(time) unless $self->_date; + return "invalid discount_term" + if ($self->discount_term && $self->discount_term < 2); + #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it # # UNIQUE index should catch this too, without race conditions, but this # # should give a better error message the other 99.9% of the time... diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index e93476dce..3e37ec9e9 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1371,6 +1371,18 @@ sub calc_recur { $self->part_pkg->calc_recur($self, @_); } +=item base_recur + +Calls the I of the FS::part_pkg object associated with this billing +item. + +=cut + +sub base_recur { + my $self = shift; + $self->part_pkg->base_recur($self, @_); +} + =item calc_remain Calls the I of the FS::part_pkg object associated with this diff --git a/FS/FS/discount.pm b/FS/FS/discount.pm index 8afeb2e0c..4f42c5b72 100644 --- a/FS/FS/discount.pm +++ b/FS/FS/discount.pm @@ -133,6 +133,17 @@ sub check { ; return $error if $error; + #discourage non-integer months for package discounts + if ($self->discountnum) { + my $sql = + "SELECT count(*) FROM part_pkg_discount WHERE part_pkg_discount.discountnum = ". + $self->discountnum; + + my $count = $self->scalar_sql($sql); + return "months must be integers greater than 1" + if ( $count && ($self->ut_number('months') || $self->months < 2) ); + } + $self->SUPER::check; } diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index c08188bcd..b267a62f4 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -20,6 +20,7 @@ use FS::part_pkg_taxrate; use FS::part_pkg_taxoverride; use FS::part_pkg_taxproduct; use FS::part_pkg_link; +use FS::part_pkg_discount; @ISA = qw( FS::m2m_Common FS::option_Common ); $DEBUG = 0; @@ -1126,6 +1127,18 @@ sub part_pkg_taxrate { } ); } +=item part_pkg_discount + +Returns the package to discount m2m records (see L) +for this package. + +=cut + +sub part_pkg_discount { + my $self = shift; + qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart }); +} + =item _rebless Reblesses the object into the FS::part_pkg::PLAN class (if available), where diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 18388d4c8..975e80ab3 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -185,6 +185,7 @@ sub calc_recur { } else { my $charge = $self->base_recur($cust_pkg); + $charge *= $param->{freq_override} if $param->{freq_override}; my $discount = $self->calc_discount($cust_pkg, $sdate, $details, $param); return sprintf('%.2f', $charge - $discount); @@ -198,6 +199,32 @@ sub calc_discount { my $tot_discount = 0; #UI enforces just 1 for now, will need ordering when they can be stacked + + if ( $param->{freq_override} ) { + my $real_part_pkg = new FS::part_pkg { $self->hash }; + $real_part_pkg->pkgpart($param->{real_pkgpart} || $self->pkgpart); + my @discount = grep { $_->months == $param->{freq_override} } + map { $_->discount } + $real_part_pkg->part_pkg_discount; + my $discount = shift @discount; + $param->{months} = $param->{freq_override} unless $param->{months}; + my $error; + if ($discount) { + if ($discount->months == $param->{months}) { + $cust_pkg->discountnum($discount->discountnum); + $error = $cust_pkg->insert_discount; + } else { + $cust_pkg->discountnum(-1); + foreach ( qw( amount percent months ) ) { + my $method = "discountnum_$_"; + $cust_pkg->$method($discount->$_); + } + $error = $cust_pkg->insert_discount; + } + die "error discounting using part_pkg_discount: $error" if $error; + } + } + my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active; foreach my $cust_pkg_discount ( @cust_pkg_discount ) { my $discount = $cust_pkg_discount->discount; @@ -214,7 +241,8 @@ sub calc_discount { $discount->months - $cust_pkg_discount->months_used ) : $chg_months; - my $error = $cust_pkg_discount->increment_months_used($months); + my $error = $cust_pkg_discount->increment_months_used($months) + if $cust_pkg->pkgpart == $param->{real_pkgpart}; die "error discounting: $error" if $error; $amount *= $months; diff --git a/FS/FS/part_pkg_discount.pm b/FS/FS/part_pkg_discount.pm new file mode 100644 index 000000000..2187e10b7 --- /dev/null +++ b/FS/FS/part_pkg_discount.pm @@ -0,0 +1,129 @@ +package FS::part_pkg_discount; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use FS::discount; +use FS::part_pkg; + +=head1 NAME + +FS::part_pkg_discount - Object methods for part_pkg_discount records + +=head1 SYNOPSIS + + use FS::part_pkg_discount; + + $record = new FS::part_pkg_discount \%hash; + $record = new FS::part_pkg_discount { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::part_pkg_discount object represents a link from a package definition +to a discount. This permits discounts for lengthened terms. FS::part_pkg_discount inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item pkgdiscountnum + +primary key + +=item pkgpart + +pkgpart + +=item discountnum + +discountnum + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new part_pkg_discount. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'part_pkg_discount'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('pkgdiscountnum') + || $self->ut_number('pkgpart') + || $self->ut_number('discountnum') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item discount + +Returns the discount associated with this part_pkg_discount. + +=cut + +sub discount { + my $self = shift; + qsearch('discount', { 'discountnum' => $self->discountnum }); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 6e9bafb93..56f7af0c6 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -535,3 +535,5 @@ FS/svc_CGP_Mixin.pm FS/svc_CGPRule_Mixin.pm FS/svc_cert.pm t/svc_cert.t +FS/part_pkg_discount.pm +t/part_pkg_discount.t diff --git a/FS/t/part_pkg_discount.t b/FS/t/part_pkg_discount.t new file mode 100644 index 000000000..0e408d0fd --- /dev/null +++ b/FS/t/part_pkg_discount.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::part_pkg_discount; +$loaded=1; +print "ok 1\n"; diff --git a/fs_selfservice/FS-SelfService/cgi/discount_term.html b/fs_selfservice/FS-SelfService/cgi/discount_term.html new file mode 100644 index 000000000..7d9ee4d1f --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/discount_term.html @@ -0,0 +1,17 @@ +<%= +if ( scalar(keys %discount_terms_hash) ) { + $OUT .= ''; + $OUT .= 'Prepayment for'; + $OUT .= ''; + $OUT .= ''; + $OUT .= ''; + $OUT .= ''; +} +$OUT .= ''; +%> diff --git a/fs_selfservice/FS-SelfService/cgi/make_ach_payment.html b/fs_selfservice/FS-SelfService/cgi/make_ach_payment.html index 09391e7ae..5b81b00a4 100644 --- a/fs_selfservice/FS-SelfService/cgi/make_ach_payment.html +++ b/fs_selfservice/FS-SelfService/cgi/make_ach_payment.html @@ -21,6 +21,7 @@ +<%= include('discount_term') %> <%= include('check') %> diff --git a/fs_selfservice/FS-SelfService/cgi/make_payment.html b/fs_selfservice/FS-SelfService/cgi/make_payment.html index e454647cc..645b68ec7 100644 --- a/fs_selfservice/FS-SelfService/cgi/make_payment.html +++ b/fs_selfservice/FS-SelfService/cgi/make_payment.html @@ -20,7 +20,9 @@ $"> - + +<%= include('discount_term') %> + Card type by <% FS::payby->payname($payby) %> + <% include('/elements/tr-select-discount_term.html', + 'custnum' => $custnum, + 'cgi' => $cgi + ) + %> + % if ( $payby eq 'BILL' ) { Check # diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi index deefa9cc1..9144c4995 100755 --- a/httemplate/edit/part_pkg.cgi +++ b/httemplate/edit/part_pkg.cgi @@ -45,6 +45,7 @@ 'agentnum' => 'Agent', 'setup_fee' => 'Setup fee', 'recur_fee' => 'Recurring fee', + 'discountnum' => 'Offer discounts for longer terms', 'bill_dst_pkgpart' => 'Include line item(s) from package', 'svc_dst_pkgpart' => 'Include services of package', 'report_option' => 'Report classes', @@ -94,6 +95,7 @@ type => 'selectlayers-select', options => [ keys %plan_labels ], labels => \%plan_labels, + onchange => 'aux_planchanged(what);', }, { field => 'setup_fee', type => 'money', @@ -195,6 +197,21 @@ 'multiple' => 1, }, + { 'type' => 'tablebreak-tr-title', + 'value' => 'Term discounts', + }, + { 'field' => 'discountnum', + 'type' => 'select-table', + 'table' => 'discount', + 'name_col' => 'name', + 'hashref' => { %$discountnum_hashref }, + #'extra_sql' => 'AND (months IS NOT NULL OR months != 0)', + 'empty_label'=> 'Select discount', + 'm2_label' => 'Offer discounts for longer terms', + 'm2m_method' => 'part_pkg_discount', + 'm2m_dstcol' => 'discountnum', + 'm2_error_callback' => $discount_error_callback, + }, { 'type' => 'tablebreak-tr-title', 'value' => 'Pricing add-ons', @@ -426,6 +443,23 @@ my $clone_callback = sub { $recur_disabled = $object->freq ? 0 : 1; }; +my $discount_error_callback = sub { + my( $cgi, $object ) = @_; + map { + if ( /^discountnum(\d+)$/ && + ( my $discountnum = $cgi->param("discountnum$1") ) ) + { + new FS::part_pkg_discount { + 'pkgpart' => $object->pkgpart, + 'discountnum' => $discountnum, + }; + } else { + (); + } + } + $cgi->param; +}; + my $m2_error_callback_maker = sub { my $link_type = shift; #yay closures return sub { @@ -484,6 +518,22 @@ my $javascript = <<'END'; } + function aux_planchanged(what) { + + alert('called!'); + var plan = what.options[what.selectedIndex].value; + var table = document.getElementById('TableNumber7') // XXX NOT ROBUST + + if ( plan == 'flat' || plan == 'prorate' || plan == 'subscription' ) { + //table.disabled = false; + table.style.visibility = ''; + } else { + //table.disabled = true; + table.style.visibility = 'hidden'; + } + + } + END @@ -736,4 +786,9 @@ my $field_callback = sub { } }; +my $discountnum_hashref = { + 'disabled' => '', + 'months' => { 'op' => '>', 'value' => 1 }, + }; + diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi index df506c677..c8b0aa7df 100755 --- a/httemplate/edit/process/cust_pay.cgi +++ b/httemplate/edit/process/cust_pay.cgi @@ -47,7 +47,7 @@ my $new = new FS::cust_pay ( { map { $_, scalar($cgi->param($_)); } qw( paid payby payinfo paybatch - pkgnum + pkgnum discount_term ) #} fields('cust_pay') } ); diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi index c0febf828..08cc14086 100755 --- a/httemplate/edit/process/part_pkg.cgi +++ b/httemplate/edit/process/part_pkg.cgi @@ -160,6 +160,12 @@ my @process_m2m = ( 'target_table' => 'tax_class', 'params' => \@tax_overrides, }, + { 'link_table' => 'part_pkg_discount', + 'target_table' => 'discount', + 'params' => [ map $cgi->param($_), + grep /^discountnum/, $cgi->param + ], + }, { 'link_table' => 'part_pkg_link', 'target_table' => 'part_pkg', 'base_field' => 'src_pkgpart', diff --git a/httemplate/elements/customer-table.html b/httemplate/elements/customer-table.html index f00419f9c..3c3f8b2ee 100644 --- a/httemplate/elements/customer-table.html +++ b/httemplate/elements/customer-table.html @@ -22,6 +22,7 @@ Example: ### 'name_singular' => 'customer', #label + 'custnum_update_callback' => 'name_of_js_callback' #passed a rownum #listrefs 'types' => ['immutable', ''], # immutable or ''/text @@ -98,6 +99,9 @@ Example: if ( name.length > 0 ) { customer.value = name; customer.setAttribute('magic', 'nosearch'); +% if ( $opt{custnum_update_callback} ) { + <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') +% } } else { customer.value = 'Not found'; customer.style.color = '#ff0000'; @@ -162,6 +166,9 @@ Example: customer_obj.style.display = ''; customer_select.style.display = 'none'; +% if ( $opt{custnum_update_callback} ) { + <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') +% } } else { @@ -223,6 +230,10 @@ Example: this.style.display = 'none'; customer_obj.style.display = ''; +% if ( $opt{custnum_update_callback} ) { + <% $opt{custnum_update_callback} %>(searchrow, '<% $opt{prefix} %>') +% } + } } @@ -314,7 +325,7 @@ Example: > % } elsif ($types->[$col] eq 'immutable') { <% $font %><% $value %><% $font ? '' : '' %> - + % } else { Cannot represent unknown type: <% $types->[$col] %> % } diff --git a/httemplate/elements/select-discount_term.html b/httemplate/elements/select-discount_term.html new file mode 100644 index 000000000..26d877a86 --- /dev/null +++ b/httemplate/elements/select-discount_term.html @@ -0,0 +1,32 @@ +% if ( scalar(@discount_term) ) { + +% } +<%init> + +my %opt = @_; + +my $cgi = $opt{'cgi'}; + +my @discount_term; +if ( $opt{discount_term} ) { + + @discount_term = @{ $opt{discount_term} }; + +} else { + + my $custnum = $opt{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "unknown custnum $custnum\n"; + + @discount_term = $cust_main->discount_terms; + +} + + diff --git a/httemplate/elements/tr-select-discount_term.html b/httemplate/elements/tr-select-discount_term.html new file mode 100644 index 000000000..58582675d --- /dev/null +++ b/httemplate/elements/tr-select-discount_term.html @@ -0,0 +1,25 @@ +% if ( scalar(@discount_term) ) { + + Prepayment for + + <% include('select-discount_term.html', + 'discount_term' => \@discount_term, + 'cgi' => $opt{'cgi'}, + ) + %> + + + +% } + +<%init> +my %opt = @_; + +my $custnum = $opt{'custnum'}; + +my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "unknown custnum $custnum\n"; + +my @discount_term = $cust_main->discount_terms; + + diff --git a/httemplate/misc/batch-cust_pay.html b/httemplate/misc/batch-cust_pay.html index 505f2d0ff..610f6e1db 100644 --- a/httemplate/misc/batch-cust_pay.html +++ b/httemplate/misc/batch-cust_pay.html @@ -13,23 +13,63 @@ function warnUnload() { } } window.onbeforeunload = warnUnload; + +function select_discount_term(row, prefix) { + var custnum_obj = document.getElementById('custnum'+prefix+row); + var select_obj = document.getElementById('discount_term'+prefix+row); + + var value = ''; + if (select_obj.type == 'hidden') { + value = select_obj.value; + } + + var term_select = document.createElement('SELECT'); + term_select.setAttribute('name', 'discount_term'+row); + term_select.setAttribute('id', 'discount_term'+row); + term_select.setAttribute('rownum', row); + term_select.style.display = ''; + select_obj.parentNode.replaceChild(term_select, select_obj); + opt(term_select, '', '1 month'); + + function select_discount_term_update(discount_terms) { + + var termArray = eval('(' + discount_terms + ')'); + for ( var t = 0; t < termArray.length; t++ ) { + opt(term_select, termArray[t][0], termArray[t][1]); + if (termArray[t][0] == value) { + term_select.selectedIndex = t+1; + } + } + + } + + discount_terms(custnum_obj.value, select_discount_term_update); + +} +<% include('/elements/xmlhttp.html', + 'url' => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi', + 'subs' => [qw( discount_terms )], + ) +%> +
<% include( "/elements/customer-table.html", name_singular => 'payment', - header => [ '', 'Amount', 'Check #', '' ], - fields => [ sub {'$'}, 'paid', 'payinfo', 'error', ], - types => [ 'immutable', '', '', 'immutable', ], - align => [ 'c', 'r', 'r', 'l' ], - sizes => [ 0, 8, 10, 0, ], - colors => [ '', '', '', '#ff0000' ], - param => { () }, - footer => [ '$', '_TOTAL', '', '' ], - footer_align => [ 'c', 'r', 'r', '' ], + header => \@header, + fields => \@fields, + types => \@types, + align => \@align, + sizes => \@sizes, + colors => \@colors, + param => \%param, + footer => \@footer, + footer_align => \@footer_align, + custnum_update_callback => $custnum_update_callback, ) %> @@ -41,6 +81,14 @@ window.onbeforeunload = warnUnload;
+%if ( $cgi->param('error') ) { + +%} + <% include('/elements/footer.html') %> <%init> @@ -48,4 +96,36 @@ window.onbeforeunload = warnUnload; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch'); +my @header = ( '', 'Amount', 'Check #' ); +my @fields = ( sub {'$'}, 'paid', 'payinfo' ); +my @types = ( 'immutable', '', '' ); +my @align = ( 'c', 'r', 'r' ); +my @sizes = ( 0, 8, 10 ); +my @colors = ( '', '', '' ); +my %param = (); +my @footer = ( '$', '_TOTAL', '' ); +my @footer_align = ( 'c', 'r', 'r' ); +my $custnum_update_callback = ''; + +if ( FS::Record->scalar_sql('SELECT count(*) FROM part_pkg_discount') ) { + push @header, ''; + push @fields, 'discount_term'; + push @types, 'immutable'; + push @align, 'r'; + push @sizes, '0'; + push @colors, ''; + push @footer, ''; + push @footer_align, ''; + $custnum_update_callback = 'select_discount_term'; +} + +push @header, ''; +push @fields, 'error'; +push @types, 'immutable'; +push @align, 'l'; +push @sizes, '0'; +push @colors, '#ff0000'; +push @footer, ''; +push @footer_align, ''; + diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index 813b560bd..bcab68aae 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -67,6 +67,11 @@ % } +<% include('/elements/tr-select-discount_term.html', + 'custnum' => $custnum, + 'cgi' => $cgi + ) +%> % if ( $payby eq 'CARD' ) { % diff --git a/httemplate/misc/process/batch-cust_pay.cgi b/httemplate/misc/process/batch-cust_pay.cgi index 4da00c63d..aefc00654 100644 --- a/httemplate/misc/process/batch-cust_pay.cgi +++ b/httemplate/misc/process/batch-cust_pay.cgi @@ -11,12 +11,13 @@ % #while ( exists($param->{"custnum$row"}) ) { % for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) { % push @cust_pay, new FS::cust_pay { -% 'custnum' => $param->{"custnum$row"}, -% 'paid' => $param->{"paid$row"}, -% 'payby' => 'BILL', -% 'payinfo' => $param->{"payinfo$row"}, -% 'paybatch' => $paybatch, -% } +% 'custnum' => $param->{"custnum$row"}, +% 'paid' => $param->{"paid$row"}, +% 'payby' => 'BILL', +% 'payinfo' => $param->{"payinfo$row"}, +% 'discount_term' => $param->{"discount_term$row"}, +% 'paybatch' => $paybatch, +% } % if $param->{"custnum$row"} % || $param->{"paid$row"} % || $param->{"payinfo$row"}; diff --git a/httemplate/misc/process/payment.cgi b/httemplate/misc/process/payment.cgi index 665001ea9..c1c9071f9 100644 --- a/httemplate/misc/process/payment.cgi +++ b/httemplate/misc/process/payment.cgi @@ -119,19 +119,26 @@ if ( $payby eq 'CHEK' ) { die "unknown payby $payby"; } +$cgi->param('discount_term') =~ /^\d*$/ + or errorpage("illegal discount_term"); +my $discount_term = $1; + my $error = ''; my $paynum = ''; if ( $cgi->param('batch') ) { - $error = $cust_main->batch_card( - 'payby' => $payby, - 'amount' => $amount, - 'payinfo' => $payinfo, - 'paydate' => "$year-$month-01", - 'payname' => $payname, - map { $_ => $cgi->param($_) } - @{$payby2fields{$payby}} - ); + $error = 'Prepayment discounts not supported with batched payments' + if $discount_term; + + $error ||= $cust_main->batch_card( + 'payby' => $payby, + 'amount' => $amount, + 'payinfo' => $payinfo, + 'paydate' => "$year-$month-01", + 'payname' => $payname, + map { $_ => $cgi->param($_) } + @{$payby2fields{$payby}} + ); errorpage($error) if $error; } else { @@ -146,6 +153,7 @@ if ( $cgi->param('batch') ) { 'payunique' => $payunique, 'paycvv' => $paycvv, 'paynum_ref' => \$paynum, + 'discount_term' => $discount_term, map { $_ => $cgi->param($_) } @{$payby2fields{$payby}} ); errorpage($error) if $error; diff --git a/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi b/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi new file mode 100644 index 000000000..71e2da597 --- /dev/null +++ b/httemplate/misc/xmlhttp-cust_main-discount_terms.cgi @@ -0,0 +1,24 @@ +% if ( $sub eq 'discount_terms' ) { +% +% my $return = []; +% my $custnum = $cgi->param('arg'); +% my $cust_main = ''; +% $cust_main = qsearchs({ +% 'table' => 'cust_main', +% 'hashref' => { 'custnum' => $custnum }, +% 'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql, +% }); +% +% if ($cust_main) { +% $return = [ map [ $_, "$_ months" ], $cust_main->discount_terms ]; +% } +% +<% objToJson($return) %> +% } +<%init> + +my $conf = new FS::Conf; + +my $sub = $cgi->param('sub'); + + diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html index 3c486dd25..3b58f9ec0 100644 --- a/httemplate/view/cust_main/packages/package.html +++ b/httemplate/view/cust_main/packages/package.html @@ -39,6 +39,7 @@ % if ( $curuser->access_right('Discount customer package') % && $part_pkg->can_discount % && ! scalar($cust_pkg->cust_pkg_discount_active) +% && ! scalar($cust_pkg->part_pkg->part_pkg_discount) % ) % { % $br=1; -- 2.11.0