Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 5 Nov 2015 18:44:38 +0000 (10:44 -0800)
committerIvan Kohler <ivan@freeside.biz>
Thu, 5 Nov 2015 18:44:38 +0000 (10:44 -0800)
34 files changed:
FS/FS/AccessRight.pm
FS/FS/Schema.pm
FS/FS/Upgrade.pm
FS/FS/access_right.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_void.pm
FS/FS/cust_bill_void.pm
FS/FS/cust_credit.pm
FS/FS/cust_credit_void.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_void.pm
FS/FS/reason_Mixin.pm
FS/FS/reason_type.pm
FS/bin/freeside-paymentech-download
FS/bin/freeside-paymentech-upload
httemplate/elements/menu.html
httemplate/elements/tr-select-reason.html
httemplate/misc/process/void-cust_bill.html
httemplate/misc/unapply-cust_credit.cgi
httemplate/misc/unapply-cust_pay.cgi
httemplate/misc/unvoid-cust_pay_void.cgi
httemplate/misc/void-cust_bill.cgi [new file with mode: 0755]
httemplate/misc/void-cust_credit.cgi [new file with mode: 0755]
httemplate/misc/void-cust_credit.html [deleted file]
httemplate/misc/void-cust_pay.cgi
httemplate/search/report_tax-xls.cgi
httemplate/view/cust_bill.cgi
httemplate/view/cust_main/payment_history/credit.html
httemplate/view/cust_main/payment_history/invoice.html
httemplate/view/cust_main/payment_history/payment.html
httemplate/view/cust_main/payment_history/voided_invoice.html
httemplate/view/cust_main/payment_history/voided_payment.html

index 95cf29a..a96a6cb 100644 (file)
@@ -232,7 +232,7 @@ tie my %rights, 'Tie::IxHash',
     'Refund Echeck payment',
     'Delete refund', #NEW
     'Add on-the-fly credit reason', #NEW
-    'Add on-the-fly void credit reason',
+    'Add on-the-fly void reason',
     'Add on-the-fly refund reason', #NEW
   ],
   
index 447302b..6b5d658 100644 (file)
@@ -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',
@@ -2523,6 +2531,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',
@@ -2544,6 +2553,9 @@ sub tables_hashref {
                           { columns    => [ 'gatewaynum' ],
                             table      => 'payment_gateway',
                           },
+                          { columns    => [ 'reasonnum' ],
+                            table      => 'reason',
+                          },
                           { columns    => [ 'void_usernum' ],
                             table      => 'access_user',
                             references => [ 'usernum' ],
index 263230b..bffda56 100644 (file)
@@ -350,6 +350,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'  => [],
 
index 40475ec..0da718c 100644 (file)
@@ -251,9 +251,7 @@ sub _upgrade_data { # class method
     'List customers' => 'List contacts',
     'Backdate payment' => 'Backdate credit',
     'Generate quotation' => 'Disable quotation',
-    'Void credit' => 'Void credit',
-    'Unvoid credit' => 'Unvoid credit',
-    'Add on-the-fly void credit reason' => 'Add on-the-fly void credit reason',
+    'Add on-the-fly void credit reason' => 'Add on-the-fly void reason',
     '_ALL' => 'Employee preference telephony integration',
   );
 
index 6546bfa..138d0fa 100644 (file)
@@ -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;
index 5861ee4..aea776a 100644 (file)
@@ -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;
index 080452e..991dd37 100644 (file)
@@ -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<FS::reson>)
 
 =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<FS::reason>) 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
index f3dba90..50f69c9 100644 (file)
@@ -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<FS::reson>)
 
 =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<FS::reason>) 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
index 2f2338e..0329535 100644 (file)
@@ -377,7 +377,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;
index 9c92068..60beaa6 100644 (file)
@@ -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
index 403d8dd..c700cf7 100644 (file)
@@ -267,7 +267,10 @@ sub _bop_defaults {
     }
   }
 
-  $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
+  unless ( exists( $options->{'payinfo'} ) ) {
+    $options->{'payinfo'} = $self->payinfo;
+    $options->{'paymask'} = $self->paymask;
+  }
 
   # Default invoice number if the customer has exactly one open invoice.
   if( ! $options->{'invnum'} ) {
@@ -889,7 +892,7 @@ sub _realtime_bop_result {
        '_date'    => '',
        'payby'    => $cust_pay_pending->payby,
        'payinfo'  => $options{'payinfo'},
-       'paymask'  => $options{'paymask'},
+       'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
        'paydate'  => $cust_pay_pending->paydate,
        'pkgnum'   => $cust_pay_pending->pkgnum,
        'discount_term'  => $options{'discount_term'},
index d9ae0d3..4d06862 100644 (file)
@@ -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 =
@@ -1023,6 +1034,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;
 
   ##
index b2f777b..8fd5396 100644 (file)
@@ -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<FS::reson>)
 
 =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,18 @@ sub void_access_user {
   qsearchs('access_user', { 'usernum' => $self->void_usernum } );
 }
 
+=item reason
+
+Returns the text of the associated void reason (see L<FS::reason>) for this.
+
+=cut
+
 # Used by FS::Upgrade to migrate to a new database.
 sub _upgrade_data {  # class method
   my ($class, %opts) = @_;
 
+  $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;
 
index a397541..af9aa50 100644 (file)
@@ -6,18 +6,21 @@ 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<FS::reason>) for this credit.
+Returns the text of the associated reason (see L<FS::reason>) for this credit /
+voided payment / voided invoice.
 
 =cut
 
 sub reason {
-  my ($self, $value, %options) = @_;
+  my $self = shift;
+
   my $reason_text;
   if ( $self->reasonnum ) {
     my $reason = FS::reason->by_key($self->reasonnum);
@@ -36,61 +39,117 @@ sub reason {
 # 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);
+sub _upgrade_reasonnum {    # class method
+    my $class = shift;
+    my $table = $class->table;
+
+    for my $fieldname (qw(reason void_reason)) {
+        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 (
+                scalar(
+                    grep { $_->getfield($fieldname) =~ /\S/ }
+                      @legacy_reason_records
+                )
+              )
+            {
+                warn "$me Found unmigrated reasons\n" if $DEBUG;
+
+                my $reason_type =
+                  _upgrade_get_legacy_reason_type( $class, $table );
+                my $noreason = _upgrade_get_no_reason( $class, $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 =
+                          _upgrade_get_reason( $class, $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;
-
-        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 $table = shift;
+
+    my $reason_class =
+      ( $table =~ /void/ ) ? 'X' : 'F';    # see FS::reason_type (%class_name)
+    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 _upgrade_get_reason( $class, '(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;
index 17a7167..4042972 100644 (file)
@@ -11,7 +11,7 @@ our %class_name = (
   'R' => 'credit',
   'S' => 'suspend',
   'F' => 'refund',
-  'X' => 'void credit',
+  'X' => 'void', # credit/invoice/payment
 );
 
 our %class_purpose = (  
@@ -19,7 +19,7 @@ our %class_purpose = (
   'R' => 'explain why a customer was credited',
   'S' => 'explain why a customer package was suspended',
   'F' => 'explain why a customer was refunded',
-  'X' => 'explain why a credit was voided',
+  'X' => 'explain why a transaction was voided',
 );
 
 =head1 NAME
index 1b2f951..9a1f609 100755 (executable)
@@ -20,7 +20,7 @@ getopts('vta:');
 
 sub log_and_die {
   my $message = shift;
-  my $log = FS::Log->new('freeside-paymenttech-download');
+  my $log = FS::Log->new('freeside-paymentech-download');
   $log->error($message);
   die $message; 
 }
index a6e6a5d..5ae147d 100755 (executable)
@@ -20,7 +20,7 @@ getopts('avtp:');
 
 sub log_and_die {
   my $message = shift;
-  my $log = FS::Log->new('freeside-paymenttech-upload');
+  my $log = FS::Log->new('freeside-paymentech-upload');
   $log->error($message);
   die $message; 
 }
index dcc02c2..81c1212 100644 (file)
@@ -721,6 +721,10 @@ if ( $curuser->access_right('Configuration') ) {
   $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{'separator6'} = ''; #its a separator!
+  $config_billing{'Void reasons'}  = [ $fsurl.'browse/reason.html?class=X', 'Void reasons explain why a void was issued.' ];
+  $config_billing{'Void reason types'}  = [ $fsurl.'browse/reason_type.html?class=X', 'Void reason types define groups of reasons.' ];
 }
 
 #XXX also to be unified
index 1258746..37a34ba 100755 (executable)
@@ -199,7 +199,7 @@ if ($class eq 'C') {
 } 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';
+  $add_access_right = 'Add on-the-fly void reason';
 } else {
   die "illegal class: $class";
 }
index 7773b0b..c0f432b 100755 (executable)
@@ -1,6 +1,6 @@
 %if ( $error ) {
 %  $cgi->param('error', $error);
-<% $cgi->redirect(popurl(2). "void-cust_bill.html?". $cgi->query_string ) %>
+<% $cgi->redirect(popurl(2). "void-cust_bill.cgi?". $cgi->query_string ) %>
 %} else {
 <& /elements/header-popup.html, 'Invoice voided' &>
 <SCRIPT TYPE="text/javascript">
index ed739ac..aa1a3a9 100755 (executable)
@@ -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"
index b0343d0..34c1ecf 100755 (executable)
@@ -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"
index 4726ee5..84b7879 100755 (executable)
@@ -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.cgi b/httemplate/misc/void-cust_bill.cgi
new file mode 100755 (executable)
index 0000000..213cf95
--- /dev/null
@@ -0,0 +1,46 @@
+<& /elements/header-popup.html, mt('Void invoice') &>
+
+<% include('/elements/error.html') %>
+
+<% emt('Are you sure you want to void this invoice?') %>
+<BR><BR>
+
+<% emt("Invoice #[_1] ([_2])",$cust_bill->display_invnum, $money_char. $cust_bill->owed) %>
+<BR><BR>
+
+<FORM METHOD="POST" ACTION="process/void-cust_bill.html">
+<INPUT TYPE="hidden" NAME="invnum" VALUE="<% $invnum %>">
+
+<% ntable("#cccccc", 2) %>
+<& /elements/tr-select-reason.html,
+             'field'          => 'reasonnum',
+             'reason_class'   => 'X',
+             'cgi'            => $cgi
+&>
+
+</TABLE>
+
+<BR>
+<CENTER>
+<BUTTON TYPE="submit">Yes, void invoice</BUTTON>&nbsp;&nbsp;&nbsp;\
+<BUTTON TYPE="button" onClick="parent.cClick();">No, do not void invoice</BUTTON>
+</CENTER>
+
+</FORM>
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Void invoices');
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+#untaint invnum
+$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum";
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+
+</%init>
diff --git a/httemplate/misc/void-cust_credit.cgi b/httemplate/misc/void-cust_credit.cgi
new file mode 100755 (executable)
index 0000000..f352e60
--- /dev/null
@@ -0,0 +1,74 @@
+%if ( $success ) {
+<& /elements/header-popup.html, mt("Credit voided") &>
+  <SCRIPT TYPE="text/javascript">
+    window.top.location.reload();
+  </SCRIPT>
+  </BODY>
+</HTML>
+%} else {
+<& /elements/header-popup.html, mt('Void credit')  &>
+
+<& /elements/error.html &>
+
+<P ALIGN="center"><B><% mt('Void this credit?') |h %></B>
+
+<FORM action="<% ${p} %>misc/void-cust_credit.cgi">
+<INPUT TYPE="hidden" NAME="crednum" VALUE="<% $crednum %>">
+
+<TABLE BGCOLOR="#cccccc" BORDER="0" CELLSPACING="2" STYLE="margin-left:auto; margin-right:auto">
+<& /elements/tr-select-reason.html,
+             'field'          => 'reasonnum',
+             'reason_class'   => 'X',
+             'cgi'            => $cgi
+&>
+</TABLE>
+
+<BR>
+<P ALIGN="CENTER">
+<INPUT TYPE="submit" NAME="confirm_void_credit" VALUE="<% mt('Void credit') |h %>"> 
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<INPUT TYPE="BUTTON" VALUE="<% mt("Don't void credit") |h %>" onClick="parent.cClick();"> 
+
+</FORM>
+</BODY>
+</HTML>
+
+%}
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Void credit');
+
+#untaint crednum
+my $crednum = $cgi->param('crednum');
+if ($crednum) {
+  $crednum =~ /^(\d+)$/ || die "Illegal crednum";
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)/ || die "Illegal crednum";
+  $crednum = $1;
+}
+
+my $cust_credit = qsearchs('cust_credit',{'crednum'=>$crednum}) || die "Credit not found";
+
+my $success = 0;
+if ($cgi->param('confirm_void_credit')) {
+
+  #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_credit->void($reason) unless $error;
+  }
+
+  if ($error) {
+    $cgi->param('error',$error);
+  } else {
+    $success = 1;
+  }
+}
+
+</%init>
diff --git a/httemplate/misc/void-cust_credit.html b/httemplate/misc/void-cust_credit.html
deleted file mode 100755 (executable)
index 1e71f00..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-%if ( $success ) {
-<& /elements/header-popup.html, mt("Credit voided") &>
-  <SCRIPT TYPE="text/javascript">
-    window.top.location.reload();
-  </SCRIPT>
-  </BODY>
-</HTML>
-%} else {
-<& /elements/header-popup.html, mt('Void credit')  &>
-
-<& /elements/error.html &>
-
-<P ALIGN="center"><B><% mt('Void this credit?') |h %></B>
-
-<FORM action="<% ${p} %>misc/void-cust_credit.html">
-<INPUT TYPE="hidden" NAME="crednum" VALUE="<% $crednum %>">
-
-<TABLE BGCOLOR="#cccccc" BORDER="0" CELLSPACING="2" STYLE="margin-left:auto; margin-right:auto">
-<& /elements/tr-select-reason.html,
-             'field'          => 'reasonnum',
-             'reason_class'   => 'X',
-             'cgi'            => $cgi
-&>
-</TABLE>
-
-<BR>
-<P ALIGN="CENTER">
-<INPUT TYPE="submit" NAME="confirm_void_credit" VALUE="<% mt('Void credit') |h %>"> 
-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-<INPUT TYPE="BUTTON" VALUE="<% mt("Don't void credit") |h %>" onClick="parent.cClick();"> 
-
-</FORM>
-</BODY>
-</HTML>
-
-%}
-<%init>
-
-die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Void credit');
-
-#untaint crednum
-my $crednum = $cgi->param('crednum');
-if ($crednum) {
-  $crednum =~ /^(\d+)$/ || die "Illegal crednum";
-} else {
-  my($query) = $cgi->keywords;
-  $query =~ /^(\d+)/ || die "Illegal crednum";
-  $crednum = $1;
-}
-
-my $cust_credit = qsearchs('cust_credit',{'crednum'=>$crednum}) || die "Credit not found";
-
-my $success = 0;
-if ($cgi->param('confirm_void_credit')) {
-
-  #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_credit->void($reason) unless $error;
-  }
-
-  if ($error) {
-    $cgi->param('error',$error);
-  } else {
-    $success = 1;
-  }
-}
-
-</%init>
index 31b7a62..784bb9a 100755 (executable)
@@ -1,16 +1,52 @@
-%if ( $error ) {
-%  errorpage($error);
+%if ( $success ) {
+<& /elements/header-popup.html, mt("Payment voided") &>
+  <SCRIPT TYPE="text/javascript">
+    window.top.location.reload();
+  </SCRIPT>
+  </BODY>
+</HTML>
 %} else {
-<% $cgi->redirect($p. "view/cust_main.cgi?". $custnum) %>
+<& /elements/header-popup.html, mt('Void payment')  &>
+
+<& /elements/error.html &>
+
+<P ALIGN="center"><B><% mt('Void this payment?') |h %></B>
+
+<FORM action="<% ${p} %>misc/void-cust_pay.cgi">
+<INPUT TYPE="hidden" NAME="paynum" VALUE="<% $paynum %>">
+
+<TABLE BGCOLOR="#cccccc" BORDER="0" CELLSPACING="2" STYLE="margin-left:auto; margin-right:auto">
+<& /elements/tr-select-reason.html,
+             'field'          => 'reasonnum',
+             'reason_class'   => 'X',
+             'cgi'            => $cgi
+&>
+</TABLE>
+
+<BR>
+<P ALIGN="CENTER">
+<INPUT TYPE="submit" NAME="confirm_void_payment" VALUE="<% mt('Void payment') |h %>"> 
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+<INPUT TYPE="BUTTON" VALUE="<% mt("Don't void payment") |h %>" onClick="parent.cClick();"> 
+
+</FORM>
+</BODY>
+</HTML>
+
 %}
 <%init>
 
 #untaint paynum
-my($query) = $cgi->keywords;
-$query =~ /^(\d+)$/ || die "Illegal paynum";
-my $paynum = $1;
+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});
+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';
@@ -19,8 +55,24 @@ $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 $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;
+  }
 
-my $error = $cust_pay->void;
+  if ($error) {
+    $cgi->param('error',$error);
+  } else {
+    $success = 1;
+  }
+}
 
 </%init>
index 07fcf7c..c914d5a 100755 (executable)
@@ -24,8 +24,9 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
   $agentname = $agent->agentname;
 }
 
-if ( $cgi->param('taxname') =~ /^([\w ]+)$/ ) {
-  $params{taxname} = $1;
+# allow anything in here; FS::Report::Tax will treat it as unsafe
+if ( length($cgi->param('taxname')) ) {
+  $params{taxname} = $cgi->param('taxname');
 } else {
   die "taxname required";
 }
index 8884dde..6d13974 100755 (executable)
@@ -9,13 +9,30 @@ function areyousure(href, message) {
 }
 </SCRIPT>
 
-% 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,
+      'action'      => $p.'misc/void-cust_bill.cgi?invnum='.$invnum,
     &>
-    <BR><BR>
+%   }
+%   if ( $can_void and $can_delete ) {
+  &nbsp;|&nbsp;
+%   }
+%   if ( $can_delete ) {
+    <A href="" onclick="areyousure(\
+      '<%$p%>misc/delete-cust_bill.html?<% $invnum %>',\
+      <% mt('Are you sure you want to delete this invoice?') |js_string %>)"\
+    TITLE = "<% mt('Delete this invoice from the database completely') |h %>">\
+    <% emt('Delete this invoice') |h %></A>
+%   }
+%   if ( $can_void or $can_delete ) {
+  <BR><BR>
+%   }
 % }
 
 % if ( $cust_bill->owed > 0
index db2e5e5..81be1cd 100644 (file)
@@ -130,7 +130,7 @@ my $void = '';
 $void = ' ('.
                include( '/elements/popup_link.html',
                     'label'    => emt('void'),
-                    'action'   => "${p}misc/void-cust_credit.html?".
+                    'action'   => "${p}misc/void-cust_credit.cgi?".
                                   $cust_credit->crednum,
                     'actionlabel' => emt('Void credit'),
                 ).
index 613936e..ca59c15 100644 (file)
@@ -27,7 +27,7 @@ if ( $cust_bill->closed !~ /^Y/i && $opt{'Void invoices'} ) {
   $void =
     ' ('. include('/elements/popup_link.html',
                     'label'     => emt('void'),
-                    'action'    => "${p}misc/void-cust_bill.html?;invnum=".
+                    'action'    => "${p}misc/void-cust_bill.cgi?;invnum=".
                                     $cust_bill->invnum,
                     'actionlabel' => emt('Void Invoice'),
                  ).
index d72e34b..7701314 100644 (file)
@@ -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! (<A HREF="${p}edit/cust_refund.cgi?payby=$1;!.
             qq!paynum=!. $cust_pay->paynum. '"'.
             qq! TITLE="! . $refundtitle
@@ -178,14 +179,17 @@ if (    $cust_pay->closed !~ /^Y/i
 }
 
 my $void = '';
-my $voidmsg = $cust_pay->payby =~ /^(CARD|CHEK)$/
+# note: "TOKN" is not yet supported in stock freeside
+my $voidmsg = $cust_pay->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.cgi?".$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'}      )
index ea61f84..ff4d12f 100644 (file)
@@ -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 %> 
 </I>
index 5c43c91..e295f9b 100644 (file)
@@ -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 %> 
 </I>