From dd9dd7a913cd8da4d97b1c72522e016562a98459 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 5 Nov 2015 16:06:56 -0800 Subject: [PATCH] Add proper reasons and reason types for payment and invoice voids. Contributed by Irina Todeva --- FS/FS/Schema.pm | 20 ++- FS/FS/Upgrade.pm | 5 + FS/FS/cust_bill.pm | 14 +- FS/FS/cust_bill_pkg.pm | 14 +- FS/FS/cust_bill_pkg_void.pm | 31 +++- FS/FS/cust_bill_void.pm | 31 +++- FS/FS/cust_credit.pm | 2 +- FS/FS/cust_credit_void.pm | 15 ++ FS/FS/cust_pay.pm | 19 ++- FS/FS/cust_pay_void.pm | 17 +- FS/FS/reason_Mixin.pm | 187 +++++++++++++++------ FS/FS/reason_type.pm | 21 ++- httemplate/browse/reason_type.html | 89 ++++++---- httemplate/elements/menu.html | 17 +- httemplate/elements/tr-select-reason.html | 19 +-- httemplate/misc/unapply-cust_credit.cgi | 2 +- httemplate/misc/unapply-cust_pay.cgi | 2 +- httemplate/misc/unvoid-cust_pay_void.cgi | 2 +- httemplate/misc/void-cust_bill.html | 9 +- httemplate/misc/void-cust_pay.cgi | 26 --- httemplate/misc/void-cust_pay.html | 78 +++++++++ httemplate/view/cust_bill.cgi | 21 ++- .../view/cust_main/payment_history/payment.html | 20 ++- .../cust_main/payment_history/voided_invoice.html | 2 +- .../cust_main/payment_history/voided_payment.html | 2 +- 25 files changed, 484 insertions(+), 181 deletions(-) mode change 100644 => 100755 httemplate/misc/void-cust_bill.html delete mode 100755 httemplate/misc/void-cust_pay.cgi create mode 100755 httemplate/misc/void-cust_pay.html diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 4fda5eb15..0e5efe90c 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -735,8 +735,9 @@ sub tables_hashref { #void fields 'void_date', @date_type, '', '', - 'reason', 'varchar', 'NULL', $char_d, '', '', - 'void_usernum', 'int', 'NULL', '', '', '', + 'reason', 'varchar', 'NULL', $char_d, '', '', + 'reasonnum', 'int', 'NULL', '', '', '', + 'void_usernum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'invnum', 'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum? huh @@ -750,6 +751,9 @@ sub tables_hashref { { columns => [ 'statementnum' ], table => 'cust_statement', #_void? both? }, + { columns => [ 'reasonnum' ], + table => 'reason', + }, { columns => [ 'void_usernum' ], table => 'access_user', references => [ 'usernum' ], @@ -1197,8 +1201,9 @@ sub tables_hashref { 'feepart', 'int', 'NULL', '', '', '', #void fields 'void_date', @date_type, '', '', - 'reason', 'varchar', 'NULL', $char_d, '', '', - 'void_usernum', 'int', 'NULL', '', '', '', + 'reason', 'varchar', 'NULL', $char_d, '', '', + 'reasonnum', 'int', 'NULL', '', '', '', + 'void_usernum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'billpkgnum', 'unique' => [], @@ -1209,6 +1214,9 @@ sub tables_hashref { { columns => [ 'invnum' ], table => 'cust_bill_void', }, + { columns => [ 'reasonnum' ], + table => 'reason', + }, #pkgnum 0 and -1 are used for special things #{ columns => [ 'pkgnum' ], # table => 'cust_pkg', @@ -2505,6 +2513,7 @@ sub tables_hashref { #void fields 'void_date', @date_type, '', '', 'reason', 'varchar', 'NULL', $char_d, '', '', + 'reasonnum', 'int', 'NULL', '', '', '', 'void_usernum', 'int', 'NULL', '', '', '', ], 'primary_key' => 'paynum', @@ -2526,6 +2535,9 @@ sub tables_hashref { { columns => [ 'gatewaynum' ], table => 'payment_gateway', }, + { columns => [ 'reasonnum' ], + table => 'reason', + }, { columns => [ 'void_usernum' ], table => 'access_user', references => [ 'usernum' ], diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index ffc04bab7..342f7bf18 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -344,6 +344,11 @@ sub upgrade_data { #customer credits 'cust_credit' => [], + # reason / void_reason migration to reasonnum / void_reasonnum + 'cust_credit_void' => [], + 'cust_bill_void' => [], + 'cust_bill_pkg_void' => [], + #duplicate history records 'h_cust_svc' => [], diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 085a9f13a..ccf141b9a 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -37,6 +37,8 @@ use FS::cust_bill_pay_pkg; use FS::cust_credit_bill_pkg; use FS::discount_plan; use FS::cust_bill_void; +use FS::reason; +use FS::reason_type; use FS::L10N; $DEBUG = 0; @@ -212,7 +214,7 @@ sub insert { } -=item void +=item void [ REASON ] Voids this invoice: deletes the invoice and adds a record of the voided invoice to the FS::cust_bill_void table (and related tables starting from @@ -224,6 +226,14 @@ sub void { my $self = shift; my $reason = scalar(@_) ? shift : ''; + unless (ref($reason) || !$reason) { + $reason = FS::reason->new_or_existing( + 'class' => 'X', + 'type' => 'Void invoice', + 'reason' => $reason + ); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -238,7 +248,7 @@ sub void { my $cust_bill_void = new FS::cust_bill_void ( { map { $_ => $self->get($_) } $self->fields } ); - $cust_bill_void->reason($reason); + $cust_bill_void->reasonnum($reason->reasonnum) if $reason; my $error = $cust_bill_void->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 5861ee47f..aea776a80 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -26,6 +26,8 @@ use FS::cust_bill_pkg_tax_location_void; use FS::cust_bill_pkg_tax_rate_location_void; use FS::cust_tax_exempt_pkg_void; use FS::cust_bill_pkg_fee_void; +use FS::reason; +use FS::reason_type; use FS::Cursor; @@ -322,7 +324,7 @@ sub insert { } -=item void +=item void [ REASON ] Voids this line item: deletes the line item and adds a record of the voided line item to the FS::cust_bill_pkg_void table (and related tables). @@ -333,6 +335,14 @@ sub void { my $self = shift; my $reason = scalar(@_) ? shift : ''; + unless (ref($reason) || !$reason) { + $reason = FS::reason->new_or_existing( + 'class' => 'X', + 'type' => 'Void invoice', + 'reason' => $reason + ); + } + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -347,7 +357,7 @@ sub void { my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( { map { $_ => $self->get($_) } $self->fields } ); - $cust_bill_pkg_void->reason($reason); + $cust_bill_pkg_void->reasonnum($reason->reasonnum) if $reason; my $error = $cust_bill_pkg_void->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm index 080452e19..991dd37dd 100644 --- a/FS/FS/cust_bill_pkg_void.pm +++ b/FS/FS/cust_bill_pkg_void.pm @@ -1,7 +1,8 @@ package FS::cust_bill_pkg_void; -use base qw( FS::TemplateItem_Mixin FS::Record ); +use base qw( FS::TemplateItem_Mixin FS::reason_Mixin FS::Record ); use strict; +use vars qw( $me $DEBUG ); use FS::Record qw( qsearch qsearchs dbh fields ); use FS::cust_bill_void; use FS::cust_bill_pkg_detail; @@ -13,6 +14,9 @@ use FS::cust_bill_pkg_tax_location; use FS::cust_bill_pkg_tax_rate_location; use FS::cust_tax_exempt_pkg; +$me = '[ FS::cust_bill_pkg_void ]'; +$DEBUG = 0; + =head1 NAME FS::cust_bill_pkg_void - Object methods for cust_bill_pkg_void records @@ -104,6 +108,13 @@ unitrecur hidden +=item reason + +freeform string (deprecated) + +=item reasonnum + +reason for voiding the payment (see L) =back @@ -134,6 +145,10 @@ sub discount_table { 'cust_bill_pkg_discount_void'; } Adds this record to the database. If there is an error, returns the error, otherwise returns false. +=item reason + +Returns the text of the associated void reason (see L) for this. + =item unvoid "Un-void"s this line item: Deletes the voided line item from the database and @@ -242,6 +257,8 @@ sub check { || $self->ut_moneyn('unitrecur') || $self->ut_enum('hidden', [ '', 'Y' ]) || $self->ut_numbern('feepart') + || $self->ut_textn('reason') + || $self->ut_foreign_keyn('reasonnum', 'reason', 'reasonnum') ; return $error if $error; @@ -266,6 +283,18 @@ sub cust_bill_pkg_fee { qsearch( 'cust_bill_pkg_fee_void', { 'billpkgnum' => $self->billpkgnum } ); } + +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. +sub _upgrade_data { # class method + my ($class, %opts) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + $class->_upgrade_reasonnum(%opts); +} + =back =head1 BUGS diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm index f3dba9081..50f69c9fa 100644 --- a/FS/FS/cust_bill_void.pm +++ b/FS/FS/cust_bill_void.pm @@ -1,13 +1,18 @@ package FS::cust_bill_void; -use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record ); +use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin + FS::reason_Mixin FS::Record ); use strict; +use vars qw( $me $DEBUG ); use FS::Record qw( qsearch qsearchs dbh fields ); use FS::cust_statement; use FS::access_user; use FS::cust_bill_pkg_void; use FS::cust_bill; +$me = '[ FS::cust_bill_void ]'; +$DEBUG = 0; + =head1 NAME FS::cust_bill_void - Object methods for cust_bill_void records @@ -82,9 +87,13 @@ promised_date void_date -=item reason +=item reason + +freeform string (deprecated) -reason +=item reasonnum + +reason for voiding the payment (see L) =item void_usernum @@ -216,6 +225,7 @@ sub check { || $self->ut_numbern('void_date') || $self->ut_textn('reason') || $self->ut_numbern('void_usernum') + || $self->ut_foreign_keyn('reasonnum', 'reason', 'reasonnum') ; return $error if $error; @@ -259,6 +269,10 @@ sub void_access_user { =item cust_bill_pkg +=item reason + +Returns the text of the associated void reason (see L) for this. + =cut sub cust_bill_pkg { #actually cust_bill_pkg_void objects @@ -339,6 +353,17 @@ sub search_sql_where { sub enable_previous { 0 } +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. +sub _upgrade_data { # class method + my ($class, %opts) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + $class->_upgrade_reasonnum(%opts); +} + =back =head1 BUGS diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index e442bdd76..836cf36a7 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -407,7 +407,7 @@ sub void { my $cust_credit_void = new FS::cust_credit_void ( { map { $_ => $self->get($_) } $self->fields } ); - $cust_credit_void->set('void_reasonnum', $reason->reasonnum); + $cust_credit_void->set('void_reasonnum', $reason->reasonnum) if $reason; my $error = $cust_credit_void->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/cust_credit_void.pm b/FS/FS/cust_credit_void.pm index 9c92068eb..60beaa655 100644 --- a/FS/FS/cust_credit_void.pm +++ b/FS/FS/cust_credit_void.pm @@ -2,12 +2,16 @@ package FS::cust_credit_void; use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::reason_Mixin FS::Record ); use strict; +use vars qw( $me $DEBUG ); use FS::Record qw(qsearchs); # qsearch qsearchs); use FS::CurrentUser; use FS::access_user; use FS::cust_credit; use FS::UID qw( dbh ); +$me = '[ FS::cust_credit_void ]'; +$DEBUG = 0; + =head1 NAME FS::cust_credit_void - Object methods for cust_credit_void objects @@ -190,6 +194,17 @@ sub void_reason { return $reason_text; } +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. +sub _upgrade_data { # class method + my ( $class, %opts ) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + $class->_upgrade_reasonnum(%opts); +} + =back =head1 BUGS diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index e8aa3c779..e34e3b221 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -2,9 +2,9 @@ package FS::cust_pay; use strict; use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin - FS::Record ); + FS::reason_Mixin FS::Record); use vars qw( $DEBUG $me $conf @encrypted_fields - $unsuspendauto $ignore_noapply + $unsuspendauto $ignore_noapply ); use Date::Format; use Business::CreditCard; @@ -24,6 +24,8 @@ use FS::cust_pkg; use FS::cust_pay_void; use FS::upgrade_journal; use FS::Cursor; +use FS::reason; +use FS::reason_type; $DEBUG = 0; @@ -438,6 +440,15 @@ adds a record of the voided payment to the FS::cust_pay_void table. sub void { my $self = shift; + my $reason = shift; + + unless (ref($reason) || !$reason) { + $reason = FS::reason->new_or_existing( + 'class' => 'X', + 'type' => 'Void payment', + 'reason' => $reason + ); + } local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -453,7 +464,7 @@ sub void { my $cust_pay_void = new FS::cust_pay_void ( { map { $_ => $self->get($_) } $self->fields } ); - $cust_pay_void->reason(shift) if scalar(@_); + $cust_pay_void->reasonnum($reason->reasonnum) if $reason; my $error = $cust_pay_void->insert; my $cust_pay_pending = @@ -1064,6 +1075,8 @@ sub _upgrade_data { #class method warn "$me upgrading $class\n" if $DEBUG; + $class->_upgrade_reasonnum(%opt); + local $FS::payinfo_Mixin::ignore_masked_payinfo = 1; ## diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm index b2f777b32..72ada2534 100644 --- a/FS/FS/cust_pay_void.pm +++ b/FS/FS/cust_pay_void.pm @@ -1,6 +1,6 @@ package FS::cust_pay_void; use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin - FS::Record ); + FS::reason_Mixin FS::Record ); use strict; use vars qw( @encrypted_fields $otaker_upgrade_kludge ); @@ -88,7 +88,9 @@ Desired pkgnum when using experimental package balances. =item void_date -=item reason +=item reason - a freeform string (deprecated) + +=item reasonnum - Reason for voiding the payment (see L) =back @@ -189,6 +191,7 @@ sub check { || $self->ut_numbern('void_date') || $self->ut_textn('reason') || $self->payinfo_check + || $self->ut_foreign_keyn('reasonnum', 'reason', 'reasonnum') ; return $error if $error; @@ -221,10 +224,20 @@ sub void_access_user { qsearchs('access_user', { 'usernum' => $self->void_usernum } ); } +=item reason + +Returns the text of the associated void reason (see L) for this. + +=cut + # Used by FS::Upgrade to migrate to a new database. sub _upgrade_data { # class method my ($class, %opts) = @_; + local $FS::payinfo_Mixin::ignore_masked_payinfo = 1; + + $class->_upgrade_reasonnum(%opts); + my $sql = "SELECT usernum FROM access_user WHERE username = ( SELECT history_user FROM h_cust_pay_void WHERE paynum = ? AND history_action = 'insert' ORDER BY history_date LIMIT 1 ) "; my $sth = dbh->prepare($sql) or die dbh->errstr; diff --git a/FS/FS/reason_Mixin.pm b/FS/FS/reason_Mixin.pm index a3975419c..9c436ab1e 100644 --- a/FS/FS/reason_Mixin.pm +++ b/FS/FS/reason_Mixin.pm @@ -6,18 +6,22 @@ use FS::Record qw( qsearch qsearchs dbdef ); use FS::access_user; use FS::UID qw( dbh ); use FS::reason; +use FS::reason_type; our $DEBUG = 0; our $me = '[FS::reason_Mixin]'; =item reason -Returns the text of the associated reason (see L) for this credit. +Returns the text of the associated reason (see L) for this credit / +voided payment / voided invoice. This can no longer be used to set the +(deprecated) free-text "reason" field; see L. =cut sub reason { - my ($self, $value, %options) = @_; + my $self = shift; + my $reason_text; if ( $self->reasonnum ) { my $reason = FS::reason->by_key($self->reasonnum); @@ -32,65 +36,136 @@ sub reason { return $reason_text; } -# it was a mistake to allow setting the reason this way; use -# FS::reason->new_or_existing - # Used by FS::Upgrade to migrate reason text fields to reasonnum. -sub _upgrade_reasonnum { # class method - my $class = shift; - my $table = $class->table; - - if (defined dbdef->table($table)->column('reason')) { - - warn "$me Checking for unmigrated reasons\n" if $DEBUG; - - my @cust_refunds = qsearch({ 'table' => $table, - 'hashref' => {}, - 'extra_sql' => 'WHERE reason IS NOT NULL', - }); - - if (scalar(grep { $_->getfield('reason') =~ /\S/ } @cust_refunds)) { - warn "$me Found unmigrated reasons\n" if $DEBUG; - my $hashref = { 'class' => 'F', 'type' => 'Legacy' }; - my $reason_type = qsearchs( 'reason_type', $hashref ); - unless ($reason_type) { - $reason_type = new FS::reason_type( $hashref ); - my $error = $reason_type->insert(); - die "$class had error inserting FS::reason_type into database: $error\n" - if $error; - } - - $hashref = { 'reason_type' => $reason_type->typenum, - 'reason' => '(none)' - }; - my $noreason = qsearchs( 'reason', $hashref ); - unless ($noreason) { - $hashref->{'disabled'} = 'Y'; - $noreason = new FS::reason( $hashref ); - my $error = $noreason->insert(); - die "can't insert legacy reason '(none)' into database: $error\n" - if $error; - } - - foreach my $cust_refund ( @cust_refunds ) { - my $reason = $cust_refund->getfield('reason'); - warn "Contemplating reason $reason\n" if $DEBUG > 1; - if ($reason =~ /\S/) { - $cust_refund->reason($reason, 'reason_type' => $reason_type->typenum) - or die "can't insert legacy reason $reason into database\n"; - }else{ - $cust_refund->reasonnum($noreason->reasonnum); +# Note that any new tables that get reasonnum fields do NOT need to be +# added here unless they have previously had a free-text "reason" field. + +sub _upgrade_reasonnum { # class method + my $class = shift; + my $table = $class->table; + + my $reason_class; + if ( $table =~ /^cust_bill/ ) { # also includes cust_bill_pkg + $reason_class = 'I'; + } elsif ( $table =~ /^cust_pay/ ) { + $reason_class = 'P'; + } elsif ( $table eq 'cust_refund' ) { + $reason_class = 'F'; + } elsif ( $table =~ /^cust_credit/ ) { + $reason_class = 'R'; + } else { + die "don't know the reason class to use for upgrading $table"; + } + + for my $fieldname (qw(reason void_reason)) { + + if ( $table =~ /^cust_credit/ and $fieldname eq 'void_reason' ) { + $reason_class = 'X'; + } + + if ( defined dbdef->table($table)->column($fieldname) + && defined dbdef->table($table)->column( $fieldname . 'num' ) ) + { + + warn "$me Checking for unmigrated reasons\n" if $DEBUG; + + my @legacy_reason_records = qsearch( + { + 'table' => $table, + 'hashref' => {}, + 'extra_sql' => 'WHERE ' . $fieldname . ' IS NOT NULL', + } + ); + + if ( @legacy_reason_records ) { + + warn "$me Found unmigrated reasons\n" if $DEBUG; + + my $reason_type = + $class->_upgrade_get_legacy_reason_type( $reason_class ); + # XXX "noreason" does not actually work, because we limited to + # "reason is not null" above. Records where the reason string + # is null will end up with a reasonnum of null also. + my $noreason = $class->_upgrade_get_no_reason( $reason_type ); + + foreach my $record_to_upgrade (@legacy_reason_records) { + my $reason = $record_to_upgrade->getfield($fieldname); + warn "Contemplating reason $reason\n" if $DEBUG > 1; + if ( $reason =~ /\S/ ) { + my $reason = + $class->_upgrade_get_reason( $reason, $reason_type ); + $record_to_upgrade->set( $fieldname . 'num', + $reason->reasonnum ); + } + else { + $record_to_upgrade->set( $fieldname . 'num', + $noreason->reasonnum ); + } + + $record_to_upgrade->setfield( $fieldname, '' ); + my $error = $record_to_upgrade->replace; + + my $primary_key = $record_to_upgrade->primary_key; + warn "*** WARNING: error replacing $fieldname in $class " + . $record_to_upgrade->get($primary_key) + . ": $error ***\n" + if $error; + } + } } + } +} - $cust_refund->setfield('reason', ''); - my $error = $cust_refund->replace; +# internal methods for upgrade - warn "*** WARNING: error replacing reason in $class ". - $cust_refund->refundnum. ": $error ***\n" - if $error; - } +# _upgrade_get_legacy_reason_type is class method supposed to be used only +# within the reason_Mixin class which will either find or create a reason_type +sub _upgrade_get_legacy_reason_type { + + my $class = shift; + my $reason_class = shift; + my $reason_type_params = { 'class' => $reason_class, 'type' => 'Legacy' }; + my $reason_type = qsearchs( 'reason_type', $reason_type_params ); + unless ($reason_type) { + $reason_type = new FS::reason_type($reason_type_params); + my $error = $reason_type->insert(); + die "$class had error inserting FS::reason_type into database: $error\n" + if $error; } - } + return $reason_type; +} + +# _upgrade_get_no_reason is class method supposed to be used only within the +# reason_Mixin class which will either find or create a default (no reason) +# reason +sub _upgrade_get_no_reason { + + my $class = shift; + my $reason_type = shift; + return $class->_upgrade_get_reason( '(none)', $reason_type ); +} + +# _upgrade_get_reason is class method supposed to be used only within the +# reason_Mixin class which will either find or create a reason +sub _upgrade_get_reason { + + my $class = shift; + my $reason_text = shift; + my $reason_type = shift; + + my $reason_params = { + 'reason_type' => $reason_type->typenum, + 'reason' => $reason_text + }; + my $reason = qsearchs( 'reason', $reason_params ); + unless ($reason) { + $reason_params->{'disabled'} = 'Y'; + $reason = new FS::reason($reason_params); + my $error = $reason->insert(); + die "can't insert legacy reason '$reason_text' into database: $error\n" + if $error; + } + return $reason; } 1; diff --git a/FS/FS/reason_type.pm b/FS/FS/reason_type.pm index 17a716712..1d049861d 100644 --- a/FS/FS/reason_type.pm +++ b/FS/FS/reason_type.pm @@ -3,15 +3,18 @@ package FS::reason_type; use strict; use vars qw( @ISA ); use FS::Record qw( qsearch qsearchs ); +use Tie::IxHash; @ISA = qw(FS::Record); -our %class_name = ( +tie our %class_name, 'Tie::IxHash', ( 'C' => 'cancel', 'R' => 'credit', 'S' => 'suspend', 'F' => 'refund', - 'X' => 'void credit', + 'X' => 'credit void', + 'I' => 'invoice void', + 'P' => 'payment void', ); our %class_purpose = ( @@ -20,6 +23,18 @@ our %class_purpose = ( 'S' => 'explain why a customer package was suspended', 'F' => 'explain why a customer was refunded', 'X' => 'explain why a credit was voided', + 'I' => 'explain why an invoice was voided', + 'P' => 'explain why a payment was voided', +); + +our %class_add_access_right = ( + 'C' => 'Add on-the-fly cancel reason', + 'R' => 'Add on-the-fly credit reason', + 'S' => 'Add on-the-fly suspend reason', + 'F' => 'Add on-the-fly refund reason', + 'X' => 'Add on-the-fly void reason', + 'I' => 'Add on-the-fly void reason', + 'P' => 'Add on-the-fly void reason', ); =head1 NAME @@ -50,7 +65,7 @@ inherits from FS::Record. The following fields are currently supported: =item typenum - primary key -=item class - currently 'C', 'R', 'S', 'F' or 'X' for cancel, credit, suspend, refund or void credit +=item class - one of the keys of %class_name =item type - name of the type of reason diff --git a/httemplate/browse/reason_type.html b/httemplate/browse/reason_type.html index 6b444bad1..0cb6e7a39 100644 --- a/httemplate/browse/reason_type.html +++ b/httemplate/browse/reason_type.html @@ -1,49 +1,62 @@ -<% include( 'elements/browse.html', - 'title' => ucfirst($classname) . " Reason Types", - 'menubar' => [ ucfirst($classname) . " reasons" => - $p.'browse/reason.html?class=' . $class, - ], - 'html_init' => $html_init, - 'name' => $classname . " reason types", - 'query' => { 'table' => 'reason_type', - 'hashref' => {}, - 'extra_sql' => $where_clause . - 'ORDER BY typenum', - }, - 'count_query' => $count_query, - 'header' => [ '#', - ucfirst($classname) . ' Reason Type', - ucfirst($classname) . ' Reasons', - ], - 'fields' => [ 'typenum', - 'type', - $reasons_sub, - ], - 'links' => [ $link, - $link, - '', - ], - ) -%> +<& elements/browse.html, + 'title' => ucwords($classname) . " Reasons", + 'html_init' => $html_init, + 'name' => $classname . " reason types", + 'query' => { 'table' => 'reason_type', + 'hashref' => {}, + 'extra_sql' => $where_clause . + 'ORDER BY typenum', + }, + 'count_query' => $count_query, + 'header' => [ '#', + ucwords($classname) . ' Reason Type', + ucwords($classname) . ' Reasons', + ], + 'fields' => [ 'typenum', + 'type', + $reasons_sub, + ], + 'links' => [ $link, + $link, + '', + ], + 'disable_total' => 1, +&> <%init> +sub ucwords { + join(' ', map ucfirst($_), split(/ /, shift)); +} + die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); $cgi->param('class') =~ /^(\w)$/ or die "illegal class"; my $class=$1; -my $classname = $FS::reason_type::class_name{$class}; +my $classname = ucfirst($FS::reason_type::class_name{$class}); + +my $html_init = 'Reasons: ' . + include('/elements/menubar.html', + map { + ucfirst($FS::reason_type::class_name{$_}), + $p.'browse/reason_type.html?class=' . $_ + } keys (%FS::reason_type::class_name) + ); -my $html_init = ucfirst($classname) . - " reason types allow groups of $classname reasons for reporting purposes." . - qq!

Add a ! . - $classname . " reason type

"; +$html_init .= '

' . + $classname . ' reasons ' . + $FS::reason_type::class_purpose{$class} . + '. Reason types allow reasons to be grouped for reporting purposes.' . + qq!

! . + ($classname =~ /^[aeiou]/i ? 'Add an ' : 'Add a ') . + lc($classname) . ' reason type'. + '

'; my $reasons_sub = sub { my $reason_type = shift; - [ map { + [ ( map { [ { 'data' => $_->reason, @@ -53,7 +66,15 @@ my $reasons_sub = sub { }, ]; } - $reason_type->enabled_reasons, + $reason_type->enabled_reasons ), + [ + { + 'data' => '(add)', + 'align' => 'left', + 'link' => $p. "edit/reason.html?class=$class", + 'data_style' => 'i', + } + ] ]; diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index ea6933198..1af6f97f8 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -621,10 +621,8 @@ $config_export_svc{'Hardware types'} = [ $fsurl.'browse/hardware_class.html', 'S if $curuser->access_right('Configuration'); tie my %config_pkg_reason, 'Tie::IxHash', - 'Cancel reasons' => [ $fsurl.'browse/reason.html?class=C', 'Cancel reasons explain why a service was cancelled.' ], - 'Cancel reason types' => [ $fsurl.'browse/reason_type.html?class=C', 'Cancel reason types define groups of reasons.' ], - 'Suspend reasons' => [ $fsurl.'browse/reason.html?class=S', 'Suspend reasons explain why a service was suspended.' ], - 'Suspend reason types' => [ $fsurl.'browse/reason_type.html?class=S', 'Suspend reason types define groups of reasons.' ], + 'Cancel reasons' => [ $fsurl.'browse/reason_type.html?class=C', 'Cancel reasons explain why a service was cancelled.' ], + 'Suspend reasons' => [ $fsurl.'browse/reason_type.html?class=S', 'Suspend reasons explain why a service was suspended.' ], ; tie my %config_pkg, 'Tie::IxHash', (); @@ -715,12 +713,13 @@ if ( $curuser->access_right('Configuration') ) { } $config_billing{'separator4'} = ''; #its a separator! - $config_billing{'Credit reasons'} = [ $fsurl.'browse/reason.html?class=R', 'Credit reasons explain why a credit was issued.' ]; - $config_billing{'Credit reason types'} = [ $fsurl.'browse/reason_type.html?class=R', 'Credit reason types define groups of reasons.' ]; + $config_billing{'Credit reasons'} = [ $fsurl.'browse/reason_type.html?class=R', 'Credit reasons explain why a credit was issued.' ]; - $config_billing{'separator5'} = ''; #its a separator! - $config_billing{'Refund reasons'} = [ $fsurl.'browse/reason.html?class=F', 'Refund reasons explain why a refund was issued.' ]; - $config_billing{'Refund reason types'} = [ $fsurl.'browse/reason_type.html?class=F', 'Refund reason types define groups of reasons.' ]; + $config_billing{'Refund reasons'} = [ $fsurl.'browse/reason_type.html?class=F', 'Refund reasons explain why a refund was issued.' ]; + + $config_billing{'Invoice void reasons'} = [ $fsurl.'browse/reason_type.html?class=I', 'Invoice void reasons explain why an invoice was voided.' ]; + $config_billing{'Payment void reasons'} = [ $fsurl.'browse/reason_type.html?class=P', 'Payment void reasons explain why a payment was voided.' ]; + $config_billing{'Credit void reasons'} = [ $fsurl.'browse/reason_type.html?class=X', 'Credit void reasons explain why a credit was voided.' ]; } #XXX also to be unified diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html index 125874694..93949ba8c 100755 --- a/httemplate/elements/tr-select-reason.html +++ b/httemplate/elements/tr-select-reason.html @@ -6,8 +6,7 @@ Example: #required 'field' => 'reasonnum', - 'reason_class' => 'C', # currently 'C', 'R', 'F', 'S' or 'X' - # for cancel, credit, refund, suspend or void credit + 'reason_class' => 'C', # one of those in %FS::reason_type::class_name #recommended 'cgi' => $cgi, #easiest way for things to be properly "sticky" on errors @@ -189,20 +188,8 @@ if ( $opt{'cgi'} ) { my $id = $opt{'id'} || $name; $id =~ s/\./_/g; # for edit/part_event -my $add_access_right; -if ($class eq 'C') { - $add_access_right = 'Add on-the-fly cancel reason'; -} elsif ($class eq 'S') { - $add_access_right = 'Add on-the-fly suspend reason'; -} elsif ($class eq 'R') { - $add_access_right = 'Add on-the-fly credit reason'; -} elsif ($class eq 'F') { - $add_access_right = 'Add on-the-fly refund reason'; -} elsif ($class eq 'X') { - $add_access_right = 'Add on-the-fly void credit reason'; -} else { - die "illegal class: $class"; -} +my $add_access_right = $FS::reason_type::class_add_access_right{$class} + or die "unknown class: $class"; my @reasons = qsearch({ 'table' => 'reason', diff --git a/httemplate/misc/unapply-cust_credit.cgi b/httemplate/misc/unapply-cust_credit.cgi index ed739ac1b..aa1a3a9c2 100755 --- a/httemplate/misc/unapply-cust_credit.cgi +++ b/httemplate/misc/unapply-cust_credit.cgi @@ -1,4 +1,4 @@ -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> +<% $cgi->redirect($p. "view/cust_main.cgi?custnum=". $custnum. ";show=payment_history") %> <%init> die "access denied" diff --git a/httemplate/misc/unapply-cust_pay.cgi b/httemplate/misc/unapply-cust_pay.cgi index b0343d034..34c1ecfd3 100755 --- a/httemplate/misc/unapply-cust_pay.cgi +++ b/httemplate/misc/unapply-cust_pay.cgi @@ -1,4 +1,4 @@ -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> +<% $cgi->redirect($p. "view/cust_main.cgi?custnum=". $custnum. ";show=payment_history") %> <%init> die "access denied" diff --git a/httemplate/misc/unvoid-cust_pay_void.cgi b/httemplate/misc/unvoid-cust_pay_void.cgi index 4726ee576..84b7879fb 100755 --- a/httemplate/misc/unvoid-cust_pay_void.cgi +++ b/httemplate/misc/unvoid-cust_pay_void.cgi @@ -1,7 +1,7 @@ %if ( $error ) { % errorpage($error); %} else { -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> +<% $cgi->redirect($p. "view/cust_main.cgi?custnum=". $custnum. ";show=payment_history") %> %} <%init> diff --git a/httemplate/misc/void-cust_bill.html b/httemplate/misc/void-cust_bill.html old mode 100644 new mode 100755 index 39b071229..e4e4705d7 --- a/httemplate/misc/void-cust_bill.html +++ b/httemplate/misc/void-cust_bill.html @@ -12,10 +12,11 @@ <% ntable("#cccccc", 2) %> - - Reason - - +<& /elements/tr-select-reason.html, + 'field' => 'reasonnum', + 'reason_class' => 'I', + 'cgi' => $cgi +&> diff --git a/httemplate/misc/void-cust_pay.cgi b/httemplate/misc/void-cust_pay.cgi deleted file mode 100755 index 31b7a6201..000000000 --- a/httemplate/misc/void-cust_pay.cgi +++ /dev/null @@ -1,26 +0,0 @@ -%if ( $error ) { -% errorpage($error); -%} else { -<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %> -%} -<%init> - -#untaint paynum -my($query) = $cgi->keywords; -$query =~ /^(\d+)$/ || die "Illegal paynum"; -my $paynum = $1; - -my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum}); - -my $right = 'Void payments'; -$right = 'Credit card void' if $cust_pay->payby eq 'CARD'; -$right = 'Echeck void' if $cust_pay->payby eq 'CHEK'; - -die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right($right); - -my $custnum = $cust_pay->custnum; - -my $error = $cust_pay->void; - - diff --git a/httemplate/misc/void-cust_pay.html b/httemplate/misc/void-cust_pay.html new file mode 100755 index 000000000..205d93aa3 --- /dev/null +++ b/httemplate/misc/void-cust_pay.html @@ -0,0 +1,78 @@ +%if ( $success ) { +<& /elements/header-popup.html, mt("Payment voided") &> + + + +%} else { +<& /elements/header-popup.html, mt('Void payment') &> + +<& /elements/error.html &> + +

<% mt('Void this payment?') |h %> + +

+ + + +<& /elements/tr-select-reason.html, + 'field' => 'reasonnum', + 'reason_class' => 'P', + 'cgi' => $cgi +&> +
+ +
+

+ +         +" onClick="parent.cClick();"> + +

+ + + +%} +<%init> + +#untaint paynum +my $paynum = $cgi->param('paynum'); +if ($paynum) { + $paynum =~ /^(\d+)$/ || die "Illegal paynum"; +} else { + my($query) = $cgi->keywords; + $query =~ /^(\d+)/ || die "Illegal paynum"; + $paynum = $1; +} + +my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum}) || die "Payment not found"; + +my $right = 'Void payments'; +$right = 'Credit card void' if $cust_pay->payby eq 'CARD'; +$right = 'Echeck void' if $cust_pay->payby eq 'CHEK'; + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right($right); + +my $success = 0; +if ($cgi->param('confirm_void_payment')) { + + #untaint reasonnum / create new reason + my ($reasonnum, $error) = $m->comp('process/elements/reason'); + if (!$reasonnum) { + $error = 'Reason required'; + } else { + my $reason = qsearchs('reason', { 'reasonnum' => $reasonnum }) + || die "Reason num $reasonnum not found in database"; + $error = $cust_pay->void($reason) unless $error; + } + + if ($error) { + $cgi->param('error',$error); + } else { + $success = 1; + } +} + + diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi index 8884ddea4..d73edbd79 100755 --- a/httemplate/view/cust_bill.cgi +++ b/httemplate/view/cust_bill.cgi @@ -9,13 +9,30 @@ function areyousure(href, message) { } -% if ( !$cust_bill->closed && $curuser->access_right('Void invoices') ) { +% if ( !$cust_bill->closed ) { # otherwise allow no changes +% my $can_delete = $conf->exists('deleteinvoices') +% && $curuser->access_right('Delete invoices'); +% my $can_void = $curuser->access_right('Void invoices'); +% if ( $can_void ) { <& /elements/popup_link.html, 'label' => emt('Void this invoice'), 'actionlabel' => emt('Void this invoice'), 'action' => $p.'misc/void-cust_bill.html?invnum='.$invnum, &> -

+% } +% if ( $can_void and $can_delete ) { +  |  +% } +% if ( $can_delete ) { + \ + <% emt('Delete this invoice') |h %> +% } +% if ( $can_void or $can_delete ) { +

+% } % } % if ( $cust_bill->owed > 0 diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index d72e34b38..6c93f7b27 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -169,8 +169,9 @@ if ( $cust_pay->closed !~ /^Y/i && scalar(@refund_right) ) { my $refundtitle = ($cust_pay->payby =~ /^(CARD|CHEK)$/) - ? emt('Send a refund for this payment to the payment gateway') - : emt('Record a refund for this payment'); + ? emt('Send a refund for this payment to the payment gateway') + : emt('Record a refund for this payment'); + $refund = qq! (payby =~ /^(CARD|CHEK|TOKN)$/ ? ' (' . emt('do not send anything to the payment gateway').')' : ''; -$void = areyousure_link("${p}misc/void-cust_pay.cgi?".$cust_pay->paynum, - emt('Are you sure you want to void this payment?'), - emt('Void this payment from the database') . $voidmsg, - emt('void') - ) +$void = ' ('. + include( '/elements/popup_link.html', + 'label' => emt('void'), + 'action' => "${p}misc/void-cust_pay.html?".$cust_pay->paynum, + 'actionlabel' => emt('Void payment'), + ). + ')' if $cust_pay->closed !~ /^Y/i && ( ( $cust_pay->payby eq 'CARD' && $opt{'Credit card void'} ) || ( $cust_pay->payby eq 'CHEK' && $opt{'Echeck void'} ) diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html index ea61f8446..ff4d12f58 100644 --- a/httemplate/view/cust_main/payment_history/voided_invoice.html +++ b/httemplate/view/cust_main/payment_history/voided_invoice.html @@ -6,7 +6,7 @@ % } % my $reason = $cust_bill_void->reason; % if ($reason) { - (<% $reason %>) + (<% $reason |h %>) % } <% mt("on [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %> diff --git a/httemplate/view/cust_main/payment_history/voided_payment.html b/httemplate/view/cust_main/payment_history/voided_payment.html index 5c43c91e5..e295f9b3b 100644 --- a/httemplate/view/cust_main/payment_history/voided_payment.html +++ b/httemplate/view/cust_main/payment_history/voided_payment.html @@ -6,7 +6,7 @@ % } % my $reason = $cust_pay_void->reason; % if ($reason) { - (<% $reason %>) + (<% $reason |h %>) % } <% mt("on [_1]", time2str($date_format, $cust_pay_void->void_date) ) |h %> -- 2.11.0