summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Conf.pm9
-rw-r--r--FS/FS/cust_main.pm255
-rw-r--r--FS/FS/cust_pay.pm18
-rwxr-xr-xhttemplate/edit/cust_refund.cgi93
-rwxr-xr-xhttemplate/edit/process/cust_credit.cgi4
-rwxr-xr-xhttemplate/edit/process/cust_refund.cgi37
-rwxr-xr-xhttemplate/view/cust_main.cgi127
7 files changed, 477 insertions, 66 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index c553f0c22..c4b214845 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -543,7 +543,7 @@ httemplate/docs/config.html
{
'key' => 'invoice_send_receipts',
'section' => 'deprecated',q
- 'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits. See the payment_receipt_email and instead.',
+ 'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits. See the payment_receipt_email and XXXX instead.',
'type' => 'checkbox',
},
@@ -1269,7 +1269,12 @@ httemplate/docs/config.html
'type' => 'checkbox',
},
-
+ {
+ 'key' => 'card_refund-days',
+ 'section' => 'billing',
+ 'description' => 'After a payment, the number of days a refund link will be available for that payment. Defaults to 120.',
+ 'type' => 'text',
+ },
);
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 740b483d5..cc9195913 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -21,6 +21,7 @@ use FS::cust_pkg;
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_pay;
+use FS::cust_pay_void;
use FS::cust_credit;
use FS::cust_refund;
use FS::part_referral;
@@ -1775,7 +1776,7 @@ sub realtime_bop {
}
my $email = $invoicing_list[0];
- my %content;
+ my %content = ();
if ( $method eq 'CC' ) {
$content{card_number} = $self->payinfo;
@@ -1808,8 +1809,7 @@ sub realtime_bop {
my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
- my $transaction =
- new Business::OnlinePayment( $processor, @bop_options );
+ my $transaction = new Business::OnlinePayment( $processor, @bop_options );
$transaction->content(
'type' => $method,
'login' => $login,
@@ -1900,6 +1900,11 @@ sub realtime_bop {
'LEC' => 'LECB',
);
+ my $paybatch = "$processor:". $transaction->authorization;
+ $paybatch .= ':'. $transaction->order_number
+ if $transaction->can('order_number')
+ && length($transaction->order_number);
+
my $cust_pay = new FS::cust_pay ( {
'custnum' => $self->custnum,
'invnum' => $options{'invnum'},
@@ -1907,7 +1912,7 @@ sub realtime_bop {
'_date' => '',
'payby' => $method2payby{$method},
'payinfo' => $self->payinfo,
- 'paybatch' => "$processor:". $transaction->authorization,
+ 'paybatch' => $paybatch,
} );
my $error = $cust_pay->insert;
if ( $error ) {
@@ -1962,6 +1967,235 @@ sub realtime_bop {
}
+=item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
+
+Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
+via a Business::OnlinePayment realtime gateway. See
+L<http://420.am/business-onlinepayment> for supported gateways.
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available options are: I<amount>, I<reason>, I<paynum>
+
+Most gateways require a reference to an original payment transaction to refund,
+so you probably need to specify a I<paynum>.
+
+I<amount> defaults to the original amount of the payment if not specified.
+
+I<reason> specifies a reason for the refund.
+
+Implementation note: If I<amount> is unspecified or equal to the amount of the
+orignal payment, first an attempt is made to "void" the transaction via
+the gateway (to cancel a not-yet settled transaction) and then if that fails,
+the normal attempt is made to "refund" ("credit") the transaction via the
+gateway is attempted.
+
+#The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
+#I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
+#if set, will override the value from the customer record.
+
+#If an I<invnum> is specified, this payment (if sucessful) is applied to the
+#specified invoice. If you don't specify an I<invnum> you might want to
+#call the B<apply_payments> method.
+
+=cut
+
+#some false laziness w/realtime_bop, not enough to make it worth merging
+#but some useful small subs should be pulled out
+sub realtime_refund_bop {
+ my( $self, $method, %options ) = @_;
+ if ( $DEBUG ) {
+ warn "$self $method refund\n";
+ warn " $_ => $options{$_}\n" foreach keys %options;
+ }
+
+ #pre-requisites
+ die "Real-time processing not enabled\n"
+ unless $conf->exists('business-onlinepayment');
+ eval "use Business::OnlinePayment";
+ die $@ if $@;
+
+ ##overrides
+ #$self->set( $_ => $options{$_} )
+ # foreach grep { exists($options{$_}) }
+ # qw( payname address1 address2 city state zip payinfo paydate paycvv);
+
+ #load up config
+ my $bop_config = 'business-onlinepayment';
+ $bop_config .= '-ach'
+ if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+ my ( $processor, $login, $password, $unused_action, @bop_options ) =
+ $conf->config($bop_config);
+ #$action ||= 'normal authorization';
+ pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+ die "No real-time processor is enabled - ".
+ "did you set the business-onlinepayment configuration value?\n"
+ unless $processor;
+
+ my $cust_pay = '';
+ my $amount = $options{'amount'};
+ my( $pay_processor, $auth, $order_number );
+ if ( $options{'paynum'} ) {
+ warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
+ $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
+ or return "Unknown paynum $options{'paynum'}";
+ $amount ||= $cust_pay->paid;
+ $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/
+ or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+ $cust_pay->paybatch;
+ ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
+ return "processor of payment $options{'paynum'} $pay_processor does not".
+ " match current processor $processor"
+ unless $pay_processor eq $processor;
+ }
+ return "neither amount nor paynum specified" unless $amount;
+
+ #first try void if applicable
+ if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
+ my $void = new Business::OnlinePayment( $processor, @bop_options );
+ $void->content(
+ 'type' => $method,
+ 'action' => 'void',
+ 'login' => $login,
+ 'password' => $password,
+ 'order_number' => $order_number,
+ 'amount' => $amount,
+ 'authorization' => $auth,
+ 'referer' => 'http://cleanwhisker.420.am/',
+ );
+ $void->submit();
+ if ( $void->is_success ) {
+ my $error = $cust_pay->void($options{'reason'});
+ if ( $error ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH voided but database not updated - '.
+ "error voiding payment: $error";
+ warn $e;
+ return $e;
+ }
+ return '';
+ }
+ }
+
+ #massage data
+ my $address = $self->address1;
+ $address .= ", ". $self->address2 if $self->address2;
+
+ my($payname, $payfirst, $paylast);
+ if ( $self->payname && $method ne 'ECHECK' ) {
+ $payname = $self->payname;
+ $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+ or return "Illegal payname $payname";
+ ($payfirst, $paylast) = ($1, $2);
+ } else {
+ $payfirst = $self->getfield('first');
+ $paylast = $self->getfield('last');
+ $payname = "$payfirst $paylast";
+ }
+
+ my %content = ();
+ if ( $method eq 'CC' ) {
+
+ $content{card_number} = $self->payinfo;
+ $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ $content{expiration} = "$2/$1";
+
+ #$content{cvv2} = $self->paycvv
+ # if defined $self->dbdef_table->column('paycvv')
+ # && length($self->paycvv);
+
+ #$content{recurring_billing} = 'YES'
+ # if qsearch('cust_pay', { 'custnum' => $self->custnum,
+ # 'payby' => 'CARD',
+ # 'payinfo' => $self->payinfo, } );
+
+ } elsif ( $method eq 'ECHECK' ) {
+ my($account_number,$routing_code) = $self->payinfo;
+ ( $content{account_number}, $content{routing_code} ) =
+ split('@', $self->payinfo);
+ $content{bank_name} = $self->payname;
+ $content{account_type} = 'CHECKING';
+ $content{account_name} = $payname;
+ $content{customer_org} = $self->company ? 'B' : 'I';
+ $content{customer_ssn} = $self->ss;
+ } elsif ( $method eq 'LEC' ) {
+ $content{phone} = $self->payinfo;
+ }
+
+ #then try refund
+ my $refund = new Business::OnlinePayment( $processor, @bop_options );
+ $refund->content(
+ 'type' => $method,
+ 'action' => 'credit',
+ 'login' => $login,
+ 'password' => $password,
+ 'order_number' => $order_number,
+ 'amount' => $amount,
+ 'authorization' => $auth,
+ 'customer_id' => $self->custnum,
+ 'last_name' => $paylast,
+ 'first_name' => $payfirst,
+ 'name' => $payname,
+ 'address' => $address,
+ 'city' => $self->city,
+ 'state' => $self->state,
+ 'zip' => $self->zip,
+ 'country' => $self->country,
+ 'referer' => 'http://cleanwhisker.420.am/',
+ %content, #after
+ );
+ $refund->submit();
+
+ return "$processor error: ". $refund->error_message
+ unless $refund->is_success();
+
+ my %method2payby = (
+ 'CC' => 'CARD',
+ 'ECHECK' => 'CHEK',
+ 'LEC' => 'LECB',
+ );
+
+ my $paybatch = "$processor:". $refund->authorization;
+ $paybatch .= ':'. $refund->order_number
+ if $refund->can('order_number') && $refund->order_number;
+
+ while ( $cust_pay && $cust_pay->unappled < $amount ) {
+ my @cust_bill_pay = $cust_pay->cust_bill_pay;
+ last unless @cust_bill_pay;
+ my $cust_bill_pay = pop @cust_bill_pay;
+ my $error = $cust_bill_pay->delete;
+ last if $error;
+ }
+
+ my $cust_refund = new FS::cust_refund ( {
+ 'custnum' => $self->custnum,
+ 'paynum' => $options{'paynum'},
+ 'refund' => $amount,
+ '_date' => '',
+ 'payby' => $method2payby{$method},
+ 'payinfo' => $self->payinfo,
+ 'paybatch' => $paybatch,
+ 'reason' => $options{'reason'} || 'card refund',
+ } );
+ my $error = $cust_refund->insert;
+ if ( $error ) {
+ $cust_refund->paynum(''); #try again with no specific paynum
+ my $error2 = $cust_refund->insert;
+ if ( $error2 ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH refunded but database not updated - '.
+ "error inserting refund ($processor): $error2".
+ " (previously tried insert with paynum #$options{'paynum'}" .
+ ": $error )";
+ warn $e;
+ return $e;
+ }
+ }
+
+ ''; #no error
+
+}
+
=item total_owed
Returns the total owed for this customer on all invoices
@@ -2516,6 +2750,19 @@ sub cust_pay {
qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
}
+=item cust_pay_void
+
+Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
+
+=cut
+
+sub cust_pay_void {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
+}
+
+
=item cust_refund
Returns all the refunds (see L<FS::cust_refund>) for this customer.
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 3317a32bd..42e3e1e1f 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -396,7 +396,8 @@ payment.
sub cust_bill_pay {
my $self = shift;
- sort { $a->_date <=> $b->_date }
+ sort { $a->_date <=> $b->_date
+ || $a->invnum <=> $b->invnum }
qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
;
}
@@ -432,6 +433,21 @@ sub unapplied {
sprintf("%.2f", $amount );
}
+=item unrefunded
+
+Returns the amount of this payment that has not been refuned; which is
+paid minus all refund applications (see L<FS::cust_pay_refund>).
+
+=cut
+
+sub unrefunded {
+ my $self = shift;
+ my $amount = $self->paid;
+ $amount -= $_->amount foreach ( $self->cust_pay_refund );
+ sprintf("%.2f", $amount );
+}
+
+
=item cust_main
Returns the parent customer object (see L<FS::cust_main>).
diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi
new file mode 100755
index 000000000..425c17738
--- /dev/null
+++ b/httemplate/edit/cust_refund.cgi
@@ -0,0 +1,93 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my $custnum = $cgi->param('custnum');
+my $refund = $cgi->param('refund');
+my $payby = $cgi->param('payby');
+my $reason = $cgi->param('reason');
+
+my( $paynum, $cust_pay ) = ( '', '' );
+if ( $cgi->param('paynum') =~ /^(\d+)$/ ) {
+ $paynum = $1;
+ $cust_pay = qsearchs('cust_pay', { paynum=>$paynum } )
+ or die "unknown payment # $paynum";
+ $refund ||= $cust_pay->unrefunded;
+ if ( $custnum ) {
+ die "payment # $paynum is not for specified customer # $custnum"
+ unless $custnum == $cust_pay->custnum;
+ } else {
+ $custnum = $cust_pay->custnum;
+ }
+}
+die "no custnum or paynum specified!" unless $custnum;
+
+my $_date = time;
+
+my $p1 = popurl(1);
+
+print header('Refund '. ucfirst(lc($payby)). ' payment', '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+ "</FONT>"
+ if $cgi->param('error');
+print <<END, small_custview($custnum, $conf->config('countrydefault'));
+ <FORM ACTION="${p1}process/cust_refund.cgi" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="refundnum" VALUE="">
+ <INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">
+ <INPUT TYPE="hidden" NAME="paynum" VALUE="$paynum">
+ <INPUT TYPE="hidden" NAME="_date" VALUE="$_date">
+ <INPUT TYPE="hidden" NAME="payby" VALUE="$payby">
+ <INPUT TYPE="hidden" NAME="payinfo" VALUE="">
+ <INPUT TYPE="hidden" NAME="paybatch" VALUE="">
+ <INPUT TYPE="hidden" NAME="credited" VALUE="">
+ <BR>
+END
+
+if ( $cust_pay ) {
+
+ #false laziness w/FS/FS/cust_pay.pm
+ my $payby = $cust_pay->payby;
+ my $payinfo = $cust_pay->payinfo;
+ $payby =~ s/^BILL$/Check/ if $payinfo;
+ $payinfo = $cust_pay->payinfo_masked if $payby eq 'CARD';
+
+ print '<BR>Payment'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$'.
+ $cust_pay->paid. '</TD></TR>'.
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$cust_pay->_date). '</TD></TR>'.
+ '<TR><TD ALIGN="right">Method</TD><TD BGCOLOR="#ffffff">'.
+ ucfirst(lc($payby)). ' # '. $payinfo. '</TD></TR>';
+ #false laziness w/FS/FS/cust_main::realtime_refund_bop
+ if ( $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ ) {
+ my ( $processor, $auth, $order_number ) = ( $1, $2, $4 );
+ print '<TR><TD ALIGN="right">Processor</TD><TD BGCOLOR="#ffffff">'.
+ $processor. '</TD></TR>';
+ print '<TR><TD ALIGN="right">Authorization</TD><TD BGCOLOR="#ffffff">'.
+ $auth. '</TD></TR>'
+ if length($auth);
+ print '<TR><TD ALIGN="right">Order number</TD><TD BGCOLOR="#ffffff">'.
+ $order_number. '</TD></TR>'
+ if length($order_number);
+ }
+ print '</TABLE>';
+}
+
+print '<BR>Refund'. ntable("#cccccc", 2).
+ '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+ time2str("%D",$_date). '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="refund" VALUE="$refund" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="$reason"></TD></TR>!;
+
+print <<END;
+</TABLE>
+<BR>
+<INPUT TYPE="submit" VALUE="Post refund">
+ </FORM>
+ </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi
index ac92631f8..85bfd4489 100755
--- a/httemplate/edit/process/cust_credit.cgi
+++ b/httemplate/edit/process/cust_credit.cgi
@@ -3,12 +3,9 @@
$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
my $custnum = $1;
-$cgi->param('otaker',getotaker);
-
my $new = new FS::cust_credit ( {
map {
$_, scalar($cgi->param($_));
- #} qw(custnum _date amount otaker reason)
} fields('cust_credit')
} );
@@ -26,5 +23,4 @@ if ( $error ) {
print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
}
-
%>
diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi
new file mode 100755
index 000000000..fc1635781
--- /dev/null
+++ b/httemplate/edit/process/cust_refund.cgi
@@ -0,0 +1,37 @@
+<%
+
+$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or die "unknown custnum $custnum";
+
+my $error = '';
+if ( $cgi->param('payby') eq 'CARD' ) {
+ $cgi->param('refund') =~ /^(\d*)(\.\d{2})?$/
+ or die "illegal refund amount ". $cgi->param('refund');
+ my $refund = "$1$2";
+ $cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
+ my $paynum = $1;
+ my $reason = $cgi->param('reason');
+ $error = $cust_main->realtime_refund_bop( 'CC', 'amount' => $refund,
+ 'paynum' => $paynum,
+ 'reason' => $reason, );
+} else {
+ die 'unimplemented';
+ #my $new = new FS::cust_refund ( {
+ # map {
+ # $_, scalar($cgi->param($_));
+ # } ( fields('cust_refund'), 'paynum' )
+ #} );
+ #$error = $new->insert;
+}
+
+
+if ( $error ) {
+ $cgi->param('error', $error);
+ print $cgi->redirect(popurl(2). "cust_refund.cgi?". $cgi->query_string );
+} else {
+ print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
index 125c51aef..a34ddc429 100755
--- a/httemplate/view/cust_main.cgi
+++ b/httemplate/view/cust_main.cgi
@@ -36,15 +36,15 @@ print qq!<A HREF="${p}edit/cust_main.cgi?$custnum">Edit this customer</A>!;
%>
<SCRIPT>
-function cancel_areyousure(href) {
- if (confirm("Perminantly delete all services and cancel this customer?") == true)
+function areyousure(href, message) {
+ if (confirm(message) == true)
window.location.href = href;
}
</SCRIPT>
<%
-print qq! | <A HREF="javascript:cancel_areyousure('${p}misc/cust_main-cancel.cgi?$custnum')">!.
+print qq! | <A HREF="javascript:areyousure('${p}misc/cust_main-cancel.cgi?$custnum', 'Perminantly delete all services and cancel this customer?')">!.
'Cancel this customer</A>'
if $cust_main->ncancelled_pkgs;
@@ -349,21 +349,6 @@ if ( $conf->config('payby-default') ne 'HIDE' ) {
}
-%>
-
-<SCRIPT>
-function cust_pkg_areyousure(href) {
- if (confirm("Permanently delete included services and cancel this package?") == true)
- window.location.href = href;
-}
-function svc_areyousure(href) {
- if (confirm("Permanently unprovision and delete this service?") == true)
- window.location.href = href;
-}
-</SCRIPT>
-
-<%
-
print qq!<A NAME="cust_pkg">Packages</A> !,
qq!( <A HREF="!, popurl(2), qq!edit/cust_pkg.cgi?$custnum">Order and cancel packages</A> (preserves services) )!,
;
@@ -544,29 +529,6 @@ print '</TABLE>';
#end display packages
%>
-<SCRIPT>
-function cust_pay_areyousure(href) {
- if (confirm("Are you sure you want to delete this payment?")
- == true)
- window.location.href = href;
-}
-function cust_pay_unapply_areyousure(href) {
- if (confirm("Are you sure you want to unapply this payment?")
- == true)
- window.location.href = href;
-}
-function cust_credit_unapply_areyousure(href) {
- if (confirm("Are you sure you want to unapply this credit?")
- == true)
- window.location.href = href;
-}
-function cust_credit_areyousure(href) {
- if (confirm("Are you sure you want to delete this credit?")
- == true)
- window.location.href = href;
-}
-</SCRIPT>
-
<% if ( $conf->config('payby-default') ne 'HIDE' ) { %>
<BR><BR><A NAME="history"><FONT SIZE="+2">Payment History</FONT></A><BR>
@@ -660,31 +622,77 @@ function cust_credit_areyousure(href) {
}
}
+ my $refund = '';
+ my $refund_days = $conf->config('card_refund-days') || 120;
+ if ( $cust_pay->closed !~ /^Y/i
+ && $cust_pay->payby eq 'CARD'
+ && time-$cust_pay->_date < $refund_days*86400
+ && $cust_pay->unrefunded > 0
+ ) {
+ $refund = qq! (<A HREF="!. qq!${p}edit/cust_refund.cgi?payby=CARD;!.
+ qq!paynum=!. $cust_pay->paynum. qq!">refund</A>)!;
+ }
+
+ my $void = '';
+ if ( $cust_pay->closed !~ /^Y/i
+ && $cust_pay->payby ne 'CARD'
+ ) {
+ $void = qq! (<A HREF="javascript:areyousure('!.
+ qq!${p}misc/void-cust_pay.cgi?!. $cust_pay->paynum.
+ qq!', 'Are you sure you want to void this payment?')">!.
+ qq!void</A>)!;
+ }
+
my $delete = '';
if ( $cust_pay->closed !~ /^Y/i && $conf->exists('deletepayments') ) {
- $delete = qq! (<A HREF="javascript:cust_pay_areyousure('!.
+ $delete = qq! (<A HREF="javascript:areyousure('!.
qq!${p}misc/delete-cust_pay.cgi?!. $cust_pay->paynum.
- qq!')">delete</A>)!;
+ qq!', 'Are you sure you want to delete this payment?')">!.
+ qq!delete</A>)!;
}
my $unapply = '';
if ( $cust_pay->closed !~ /^Y/i
&& $conf->exists('unapplypayments')
&& scalar(@cust_bill_pay) ) {
- $unapply = qq! (<A HREF="javascript:cust_pay_unapply_areyousure('!.
+ $unapply = qq! (<A HREF="javascript:areyousure('!.
qq!${p}misc/unapply-cust_pay.cgi?!. $cust_pay->paynum.
- qq!')">unapply</A>)!;
+ qq!', 'Are you sure you want to unapply this payment?')">!.
+ qq!unapply</A>)!;
}
push @history, {
'date' => $cust_pay->_date,
'desc' => $pre. "Payment$post$info$desc".
- "$apply$delete$unapply",
+ "$apply$refund$void$delete$unapply",
'payment' => $cust_pay->paid,
'target' => $target,
};
}
+ #voided payments
+ foreach my $cust_pay_void ($cust_main->cust_pay_void) {
+
+ my $payby = $cust_pay_void->payby;
+ my $payinfo = $payby eq 'CARD'
+ ? $cust_pay_void->payinfo_masked
+ : $cust_pay_void->payinfo;
+
+ $payby =~ s/^BILL$/Check #/ if $payinfo;
+ $payby =~ s/^BILL$//;
+ $payby =~ s/^(CARD|COMP)$/$1 /;
+ my $info = $payby ? " ($payby$payinfo)" : '';
+
+ push @history, {
+ 'date' => $cust_pay_void->_date,
+ 'desc' => "Payment $info <I>voided ".
+ time2str("%D", $cust_pay_void->void_date).
+ " by ". $cust_pay_void->otaker. '</i>',
+ 'void_payment' => $cust_pay_void->paid,
+ };
+
+ }
+
#credits (some false laziness w/payments)
foreach my $cust_credit ($cust_main->cust_credit) {
@@ -740,18 +748,20 @@ function cust_credit_areyousure(href) {
#
my $delete = '';
if ( $cust_credit->closed !~ /^Y/i && $conf->exists('deletecredits') ) {
- $delete = qq! (<A HREF="javascript:cust_credit_areyousure('!.
+ $delete = qq! (<A HREF="javascript:areyousure('!.
qq!${p}misc/delete-cust_credit.cgi?!. $cust_credit->crednum.
- qq!')">delete</A>)!;
+ qq!', 'Are you sure you want to delete this credit?')">!.
+ qq!delete</A>)!;
}
my $unapply = '';
if ( $cust_credit->closed !~ /^Y/i
&& $conf->exists('unapplycredits')
&& scalar(@cust_credit_bill) ) {
- $unapply = qq! (<A HREF="javascript:cust_credit_unapply_areyousure('!.
+ $unapply = qq! (<A HREF="javascript:areyousure('!.
qq!${p}misc/unapply-cust_credit.cgi?!. $cust_credit->crednum.
- qq!')">unapply</A>)!;
+ qq!', 'Are you sure you want to unapply this credit?')">!.
+ qq!unapply</A>)!;
}
push @history, {
@@ -809,6 +819,8 @@ function cust_credit_areyousure(href) {
my $payment = exists($item->{'payment'})
? sprintf('-&nbsp;$%.2f', $item->{'payment'})
: '';
+ $payment ||= sprintf('<DEL>-&nbsp;$%.2f</DEL>', $item->{'void_payment'})
+ if exists($item->{'void_payment'});
my $credit = exists($item->{'credit'})
? sprintf('-&nbsp;$%.2f', $item->{'credit'})
: '';
@@ -976,7 +988,8 @@ sub svc_provision_link {
sub svc_unprovision_link {
my $svc = shift or return '';
- return qq!<A HREF="javascript:svc_areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}')">Unprovision</A>!;
+ qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}',!.
+ qq!'Permanently unprovision and delete this service?')">Unprovision</A>!;
}
# This should be generalized to use config options to determine order.
@@ -1004,7 +1017,8 @@ sub pkg_datestr {
sub pkg_change_link {
my $pkg = shift or return '';
- return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">Change&nbsp;package</a>!;
+ return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">!.
+ qq!Change&nbsp;package</a>!;
}
sub pkg_suspend_link {
@@ -1019,19 +1033,22 @@ sub pkg_unsuspend_link {
sub pkg_cancel_link {
my $pkg = shift or return '';
- qq!<A HREF="javascript:cust_pkg_areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}')">Cancel now</A> | !.
+ qq!<A HREF="javascript:areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}', !.
+ qq!'Permanently delete included services and cancel this package?')">!.
+ qq!Cancel now</A> | !.
qq!<A HREF="${p}misc/expire_pkg.cgi?$pkg->{pkgnum}">Cancel later</A>!;
}
sub pkg_dates_link {
my $pkg = shift or return '';
- return qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit&nbsp;dates</A>!;
+ qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit&nbsp;dates</A>!;
}
sub pkg_customize_link {
my $pkg = shift or return '';
my $custnum = shift;
- return qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};pkgnum=$pkg->{pkgnum}">Customize</A>!;
+ qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};!.
+ qq!pkgnum=$pkg->{pkgnum}">Customize</A>!;
}
%>