invoice voiding, RT#18677
authorIvan Kohler <ivan@freeside.biz>
Wed, 1 Aug 2012 06:02:14 +0000 (23:02 -0700)
committerIvan Kohler <ivan@freeside.biz>
Wed, 1 Aug 2012 06:02:14 +0000 (23:02 -0700)
19 files changed:
FS/FS/AccessRight.pm
FS/FS/Schema.pm
FS/FS/access_right.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_detail_void.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_display_void.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_tax_location_void.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_tax_rate_location_void.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_void.pm [new file with mode: 0644]
FS/FS/cust_bill_void.pm [new file with mode: 0644]
FS/FS/cust_main.pm
FS/FS/cust_tax_exempt_pkg_void.pm [new file with mode: 0644]
httemplate/misc/process/void-cust_bill.html [new file with mode: 0755]
httemplate/misc/void-cust_bill.html [new file with mode: 0644]
httemplate/view/cust_bill_void.html [new file with mode: 0755]
httemplate/view/cust_main/payment_history.html
httemplate/view/cust_main/payment_history/invoice.html
httemplate/view/cust_main/payment_history/voided_invoice.html [new file with mode: 0644]

index 4de2948..ebf66e6 100644 (file)
@@ -177,7 +177,8 @@ tie my %rights, 'Tie::IxHash',
   'Customer invoice / financial info rights' => [
     'View invoices',
     'Resend invoices', #NEWNEW
-    'Delete invoices', #new, but no need to phase in
+    'Void invoices',
+    'Delete invoices',
     'View customer tax exemptions', #yow
     'Add customer tax adjustment', #new, but no need to phase in
     'View customer batched payments', #NEW
index cfb8060..e59268b 100644 (file)
@@ -551,6 +551,35 @@ sub tables_hashref {
       'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'] ],
     },
 
+    'cust_bill_void' => {
+      'columns' => [
+        #regular fields
+        'invnum',       'int',     '', '', '', '', 
+        'custnum',      'int',     '', '', '', '', 
+        '_date',        @date_type,        '', '', 
+        'charged',      @money_type,       '', '', 
+        'invoice_terms', 'varchar', 'NULL', $char_d, '', '',
+
+        #customer balance info at invoice generation time
+        'previous_balance',   @money_typen, '', '',  #eventually not nullable
+        'billing_balance',    @money_typen, '', '',  #eventually not nullable
+
+        #specific use cases
+        'closed',      'char', 'NULL',  1, '', '', #not yet used much
+        'statementnum', 'int', 'NULL', '', '', '', #invoice aggregate statements
+        'agent_invid',  'int', 'NULL', '', '', '', #(varchar?) importing legacy
+        'promised_date', @date_type,       '', '',
+
+        #void fields
+        'void_date', @date_type, '', '', 
+        'reason',    'varchar',   'NULL', $char_d, '', '', 
+        'void_usernum',   'int', 'NULL', '', '', '',
+      ],
+      'primary_key' => 'invnum',
+      'unique' => [ [ 'custnum', 'agent_invid' ] ], #agentnum?  huh
+      'index' => [ ['custnum'], ['_date'], ['statementnum'], ['agent_invid'], [ 'void_usernum' ] ],
+    },
+
     #for importing invoices from a legacy system for display purposes only
     # no effect upon balance
     'legacy_cust_bill' => {
@@ -787,6 +816,101 @@ sub tables_hashref {
       'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ],
     },
 
+    'cust_bill_pkg_void' => {
+      'columns' => [
+        'billpkgnum',           'int',     '',      '', '', '', 
+        'invnum',               'int',     '',      '', '', '', 
+        'pkgnum',               'int',     '',      '', '', '', 
+        'pkgpart_override',     'int', 'NULL',      '', '', '', 
+        'setup',               @money_type,             '', '', 
+        'recur',               @money_type,             '', '', 
+        'sdate',               @date_type,              '', '', 
+        'edate',               @date_type,              '', '', 
+        'itemdesc',         'varchar', 'NULL', $char_d, '', '', 
+        'itemcomment',      'varchar', 'NULL', $char_d, '', '', 
+        'section',          'varchar', 'NULL', $char_d, '', '', 
+        'freq',             'varchar', 'NULL', $char_d, '', '',
+        'quantity',             'int', 'NULL',      '', '', '',
+        'unitsetup',           @money_typen,            '', '', 
+        'unitrecur',           @money_typen,            '', '', 
+        'hidden',              'char', 'NULL',       1, '', '',
+        #void fields
+        'void_date', @date_type, '', '', 
+        'reason',    'varchar',   'NULL', $char_d, '', '', 
+        'void_usernum',   'int', 'NULL', '', '', '',
+      ],
+      'primary_key' => 'billpkgnum',
+      'unique' => [],
+      'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], [ 'void_usernum' ], ],
+    },
+
+    'cust_bill_pkg_detail_void' => {
+      'columns' => [
+        'detailnum',  'int', '', '', '', '', 
+        'billpkgnum', 'int', 'NULL', '', '', '',        # should not be nullable
+        'pkgnum',  'int', 'NULL', '', '', '',           # deprecated
+        'invnum',  'int', 'NULL', '', '', '',           # deprecated
+        'amount',  'decimal', 'NULL', '10,4', '', '',
+        'format',  'char', 'NULL', 1, '', '',
+        'classnum', 'int', 'NULL', '', '', '',
+        'duration', 'int', 'NULL', '',  0, '',
+        'phonenum', 'varchar', 'NULL', 15, '', '',
+        'accountcode', 'varchar',  'NULL',      20, '', '',
+        'startdate',  @date_type, '', '', 
+        'regionname', 'varchar', 'NULL', $char_d, '', '',
+        'detail',  'varchar', '', 255, '', '', 
+      ],
+      'primary_key' => 'detailnum',
+      'unique' => [],
+      'index' => [ [ 'billpkgnum' ], [ 'classnum' ], [ 'pkgnum', 'invnum' ] ],
+    },
+
+    'cust_bill_pkg_display_void' => {
+      'columns' => [
+        'billpkgdisplaynum',    'int', '', '', '', '', 
+        'billpkgnum', 'int', '', '', '', '', 
+        'section',  'varchar', 'NULL', $char_d, '', '', 
+        #'unitsetup', @money_typen, '', '',     #override the linked real one?
+        #'unitrecur', @money_typen, '', '',     #this too?
+        'post_total', 'char', 'NULL', 1, '', '',
+        'type',       'char', 'NULL', 1, '', '',
+        'summary',    'char', 'NULL', 1, '', '',
+      ],
+      'primary_key' => 'billpkgdisplaynum',
+      'unique' => [],
+      'index' => [ ['billpkgnum'], ],
+    },
+
+    'cust_bill_pkg_tax_location_void' => {
+      'columns' => [
+        'billpkgtaxlocationnum',    'int',      '', '', '', '',
+        'billpkgnum',               'int',      '', '', '', '',
+        'taxnum',                   'int',      '', '', '', '',
+        'taxtype',              'varchar',      '', $char_d, '', '',
+        'pkgnum',                   'int',      '', '', '', '',
+        'locationnum',              'int',      '', '', '', '', #redundant?
+        'amount',                   @money_type,        '', '',
+      ],
+      'primary_key' => 'billpkgtaxlocationnum',
+      'unique' => [],
+      'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'pkgnum' ], [ 'locationnum' ] ],
+    },
+
+    'cust_bill_pkg_tax_rate_location_void' => {
+      'columns' => [
+        'billpkgtaxratelocationnum',    'int',      '', '', '', '',
+        'billpkgnum',                   'int',      '', '', '', '',
+        'taxnum',                       'int',      '', '', '', '',
+        'taxtype',                  'varchar',      '', $char_d, '', '',
+        'locationtaxid',            'varchar',  'NULL', $char_d, '', '',
+        'taxratelocationnum',           'int',      '', '', '', '',
+        'amount',                       @money_type,        '', '',
+      ],
+      'primary_key' => 'billpkgtaxratelocationnum',
+      'unique' => [],
+      'index'  => [ [ 'billpkgnum' ], [ 'taxnum' ], [ 'taxratelocationnum' ] ],
+    },
+
     'cust_credit' => {
       'columns' => [
         'crednum',  'serial', '', '', '', '', 
@@ -1419,20 +1543,29 @@ sub tables_hashref {
       'columns' => [
         'paynum',    'int',    '',   '', '', '', 
         'custnum',   'int',    '',   '', '', '', 
-        'paid',      @money_type, '', '', 
         '_date',     @date_type, '', '', 
+        'paid',      @money_type, '', '', 
+        'otaker',   'varchar', 'NULL', 32, '', '', 
+        'usernum',   'int', 'NULL', '', '', '',
         'payby',     'char',   '',     4, '', '', # CARD/BILL/COMP, should be
                                                   # index into payby table
                                                   # eventually
         'payinfo',   'varchar',   'NULL', 512, '', '', #see cust_main above
        'paymask', 'varchar', 'NULL', $char_d, '', '', 
+        #'paydate' ?
         'paybatch',  'varchar',   'NULL', $char_d, '', '', #for auditing purposes.
         'closed',    'char', 'NULL', 1, '', '', 
         'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances
+        # cash/check deposit info fields
+        'bank',       'varchar', 'NULL', $char_d, '', '',
+        'depositor',  'varchar', 'NULL', $char_d, '', '',
+        'account',    'varchar', 'NULL', 20,      '', '',
+        'teller',     'varchar', 'NULL', 20,      '', '',
+        'batchnum',       'int', 'NULL', '', '', '', #pay_batch foreign key
+
+        #void fields
         'void_date', @date_type, '', '', 
         'reason',    'varchar',   'NULL', $char_d, '', '', 
-        'otaker',   'varchar', 'NULL', 32, '', '', 
-        'usernum',   'int', 'NULL', '', '', '',
         'void_usernum',   'int', 'NULL', '', '', '',
       ],
       'primary_key' => 'paynum',
@@ -2568,6 +2701,26 @@ sub tables_hashref {
                   ],
     },
 
+    'cust_tax_exempt_pkg_void' => {
+      'columns' => [
+        'exemptpkgnum',  'int', '', '', '', '', 
+        #'custnum',      'int', '', '', '', ''
+        'billpkgnum',   'int', '', '', '', '', 
+        'taxnum',       'int', '', '', '', '', 
+        'year',         'int', '', '', '', '', 
+        'month',        'int', '', '', '', '', 
+        'creditbillpkgnum', 'int', 'NULL', '', '', '',
+        'amount',       @money_type, '', '', 
+      ],
+      'primary_key' => 'exemptpkgnum',
+      'unique' => [],
+      'index'  => [ [ 'taxnum', 'year', 'month' ],
+                    [ 'billpkgnum' ],
+                    [ 'taxnum' ],
+                    [ 'creditbillpkgnum' ],
+                  ],
+    },
+
     'router' => {
       'columns' => [
         'routernum', 'serial', '', '', '', '', 
index e6266b4..bc57364 100644 (file)
@@ -193,6 +193,7 @@ sub _upgrade_data { # class method
     'Suspend customer package'            => 'Suspend customer',
     'Unsuspend customer package'          => 'Unsuspend customer',
     'New prospect'                        => 'Generate quotation',
+    'Delete invoices'                     => 'Void invoices',
 
     'List services'    => [ 'Services: Accounts',
                             'Services: Domains',
index c3d48a6..c5b707b 100644 (file)
@@ -38,6 +38,7 @@ use FS::cust_bill_batch;
 use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
 use FS::discount_plan;
+use FS::cust_bill_void;
 use FS::L10N;
 
 $DEBUG = 0;
@@ -203,10 +204,63 @@ sub insert {
 
 }
 
+=item void
+
+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
+FS::cust_bill_pkg_void).
+
+=cut
+
+sub void {
+  my $self = shift;
+  my $reason = scalar(@_) ? shift : '';
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_bill_void = new FS::cust_bill_void ( {
+    map { $_ => $self->get($_) } $self->fields
+  } );
+  $cust_bill_void->reason($reason);
+  my $error = $cust_bill_void->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    my $error = $cust_bill_pkg->void($reason);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $error = $self->delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
 =item delete
 
 This method now works but you probably shouldn't use it.  Instead, apply a
-credit against the invoice.
+credit against the invoice, or use the new void method.
 
 Using this method to delete invoices outright is really, really bad.  There
 would be no record you ever posted this invoice, and there are no check to
@@ -236,11 +290,10 @@ sub delete {
     cust_event
     cust_credit_bill
     cust_bill_pay
-    cust_credit_bill
     cust_pay_batch
     cust_bill_pay_batch
-    cust_bill_pkg
     cust_bill_batch
+    cust_bill_pkg
   )) {
 
     foreach my $linked ( $self->$table() ) {
index 4220d3c..2ceef04 100644 (file)
@@ -3,6 +3,7 @@ package FS::cust_bill_pkg;
 use strict;
 use vars qw( @ISA $DEBUG $me );
 use Carp;
+use List::Util qw( sum );
 use Text::CSV_XS;
 use FS::Record qw( qsearch qsearchs dbdef dbh );
 use FS::cust_main_Mixin;
@@ -18,8 +19,12 @@ use FS::cust_tax_exempt_pkg;
 use FS::cust_bill_pkg_tax_location;
 use FS::cust_bill_pkg_tax_rate_location;
 use FS::cust_tax_adjustment;
-
-use List::Util qw(sum);
+use FS::cust_bill_pkg_void;
+use FS::cust_bill_pkg_detail_void;
+use FS::cust_bill_pkg_display_void;
+use FS::cust_bill_pkg_tax_location_void;
+use FS::cust_bill_pkg_tax_rate_location_void;
+use FS::cust_tax_exempt_pkg_void;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
@@ -230,6 +235,74 @@ sub insert {
 
 }
 
+=item void
+
+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).
+
+=cut
+
+sub void {
+  my $self = shift;
+  my $reason = scalar(@_) ? shift : '';
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
+    map { $_ => $self->get($_) } $self->fields
+  } );
+  $cust_bill_pkg_void->reason($reason);
+  my $error = $cust_bill_pkg_void->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $table (qw(
+    cust_bill_pkg_detail
+    cust_bill_pkg_display
+    cust_bill_pkg_tax_location
+    cust_bill_pkg_tax_rate_location
+    cust_tax_exempt_pkg
+  )) {
+
+    foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
+
+      my $vclass = 'FS::'.$table.'_void';
+      my $void = $vclass->new( {
+        map { $_ => $linked->get($_) } $linked->fields
+      });
+      my $error = $void->insert || $linked->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+
+    }
+
+  }
+
+  $error = $self->delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
 =item delete
 
 Not recommended.
diff --git a/FS/FS/cust_bill_pkg_detail_void.pm b/FS/FS/cust_bill_pkg_detail_void.pm
new file mode 100644 (file)
index 0000000..cebe7c1
--- /dev/null
@@ -0,0 +1,168 @@
+package FS::cust_bill_pkg_detail_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::usage_class;
+
+=head1 NAME
+
+FS::cust_bill_pkg_detail_void - Object methods for cust_bill_pkg_detail_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_detail_void;
+
+  $record = new FS::cust_bill_pkg_detail_void \%hash;
+  $record = new FS::cust_bill_pkg_detail_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_detail_void object represents additional detail
+information for a voided invoice line item.  FS::cust_bill_pkg_detail_void
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item detailnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item pkgnum
+
+pkgnum
+
+=item invnum
+
+invnum
+
+=item amount
+
+amount
+
+=item format
+
+format
+
+=item classnum
+
+classnum
+
+=item duration
+
+duration
+
+=item phonenum
+
+phonenum
+
+=item accountcode
+
+accountcode
+
+=item startdate
+
+startdate
+
+=item regionname
+
+regionname
+
+=item detail
+
+detail
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_detail_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('detailnum')
+    || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum')
+    || $self->ut_numbern('pkgnum')
+    || $self->ut_numbern('invnum')
+    || $self->ut_floatn('amount')
+    || $self->ut_enum('format', [ '', 'C' ] )
+    || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
+    || $self->ut_numbern('duration')
+    || $self->ut_textn('phonenum')
+    || $self->ut_textn('accountcode')
+    || $self->ut_numbern('startdate')
+    || $self->ut_textn('regionname')
+    || $self->ut_text('detail')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_display_void.pm b/FS/FS/cust_bill_pkg_display_void.pm
new file mode 100644 (file)
index 0000000..e78801a
--- /dev/null
@@ -0,0 +1,132 @@
+package FS::cust_bill_pkg_display_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+
+=head1 NAME
+
+FS::cust_bill_pkg_display_void - Object methods for cust_bill_pkg_display_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_display_void;
+
+  $record = new FS::cust_bill_pkg_display_void \%hash;
+  $record = new FS::cust_bill_pkg_display_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_display_void object represents voided line item display
+information.  FS::cust_bill_pkg_display_void inherits from FS::Record.  The
+following fields are currently supported:
+
+=over 4
+
+=item billpkgdisplaynum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item section
+
+section
+
+=item post_total
+
+post_total
+
+=item type
+
+type
+
+=item summary
+
+summary
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_display_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('billpkgdisplaynum')
+    || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum')
+    || $self->ut_textn('section')
+    || $self->ut_enum('post_total', [ '', 'Y' ])
+    || $self->ut_enum('type', [ '', 'S', 'R', 'U' ])
+    || $self->ut_enum('summary', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_tax_location_void.pm b/FS/FS/cust_bill_pkg_tax_location_void.pm
new file mode 100644 (file)
index 0000000..9e0794b
--- /dev/null
@@ -0,0 +1,139 @@
+package FS::cust_bill_pkg_tax_location_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_pkg;
+use FS::cust_location;
+
+=head1 NAME
+
+FS::cust_bill_pkg_tax_location_void - Object methods for cust_bill_pkg_tax_location_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_tax_location_void;
+
+  $record = new FS::cust_bill_pkg_tax_location_void \%hash;
+  $record = new FS::cust_bill_pkg_tax_location_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_tax_location_void object represents a voided record
+of taxation based on package location.  FS::cust_bill_pkg_tax_location_void
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item billpkgtaxlocationnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item taxtype
+
+taxtype
+
+=item pkgnum
+
+pkgnum
+
+=item locationnum
+
+locationnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_tax_location_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('billpkgtaxlocationnum')
+    || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+    || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
+    || $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
+    || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+    || $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' )
+    || $self->ut_money('amount')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_tax_rate_location_void.pm b/FS/FS/cust_bill_pkg_tax_rate_location_void.pm
new file mode 100644 (file)
index 0000000..f2e85c0
--- /dev/null
@@ -0,0 +1,139 @@
+package FS::cust_bill_pkg_tax_rate_location_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::tax_rate_location;
+
+=head1 NAME
+
+FS::cust_bill_pkg_tax_rate_location_void - Object methods for cust_bill_pkg_tax_rate_location_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_tax_rate_location_void;
+
+  $record = new FS::cust_bill_pkg_tax_rate_location_void \%hash;
+  $record = new FS::cust_bill_pkg_tax_rate_location_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_tax_rate_location_void object represents a voided record
+of taxation based on package location.
+FS::cust_bill_pkg_tax_rate_location_void inherits from FS::Record.  The
+following fields are currently supported:
+
+=over 4
+
+=item billpkgtaxratelocationnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item taxtype
+
+taxtype
+
+=item locationtaxid
+
+locationtaxid
+
+=item taxratelocationnum
+
+taxratelocationnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_tax_rate_location_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('billpkgtaxratelocationnum')
+    || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+    || $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
+    || $self->ut_text('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
+    || $self->ut_textn('locationtaxid')
+    || $self->ut_foreign_key('taxratelocationnum', 'tax_rate_location', 'taxratelocationnum' )
+    || $self->ut_money('amount')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_bill_pkg_tax_rate_location>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_void.pm b/FS/FS/cust_bill_pkg_void.pm
new file mode 100644 (file)
index 0000000..1982839
--- /dev/null
@@ -0,0 +1,181 @@
+package FS::cust_bill_pkg_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_bill_pkg_void - Object methods for cust_bill_pkg_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_void;
+
+  $record = new FS::cust_bill_pkg_void \%hash;
+  $record = new FS::cust_bill_pkg_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_void object represents a voided invoice line item.
+FS::cust_bill_pkg_void inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item billpkgnum
+
+primary key
+
+=item invnum
+
+invnum
+
+=item pkgnum
+
+pkgnum
+
+=item pkgpart_override
+
+pkgpart_override
+
+=item setup
+
+setup
+
+=item recur
+
+recur
+
+=item sdate
+
+sdate
+
+=item edate
+
+edate
+
+=item itemdesc
+
+itemdesc
+
+=item itemcomment
+
+itemcomment
+
+=item section
+
+section
+
+=item freq
+
+freq
+
+=item quantity
+
+quantity
+
+=item unitsetup
+
+unitsetup
+
+=item unitrecur
+
+unitrecur
+
+=item hidden
+
+hidden
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('billpkgnum')
+    || $self->ut_snumber('pkgnum')
+    || $self->ut_number('invnum') #cust_bill or cust_bill_void ?
+    || $self->ut_numbern('pkgpart_override')
+    || $self->ut_money('setup')
+    || $self->ut_money('recur')
+    || $self->ut_numbern('sdate')
+    || $self->ut_numbern('edate')
+    || $self->ut_textn('itemdesc')
+    || $self->ut_textn('itemcomment')
+    || $self->ut_textn('section')
+    || $self->ut_textn('freq')
+    || $self->ut_numbern('quantity')
+    || $self->ut_moneyn('unitsetup')
+    || $self->ut_moneyn('unitrecur')
+    || $self->ut_enum('hidden', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_void.pm b/FS/FS/cust_bill_void.pm
new file mode 100644 (file)
index 0000000..c782172
--- /dev/null
@@ -0,0 +1,217 @@
+package FS::cust_bill_void;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
+
+use strict;
+use FS::Record qw( qsearchs ); #qsearch );
+use FS::cust_main;
+use FS::cust_statement;
+use FS::access_user;
+
+=head1 NAME
+
+FS::cust_bill_void - Object methods for cust_bill_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_void;
+
+  $record = new FS::cust_bill_void \%hash;
+  $record = new FS::cust_bill_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_void object represents a voided invoice.  FS::cust_bill_void
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item invnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item _date
+
+_date
+
+=item charged
+
+charged
+
+=item invoice_terms
+
+invoice_terms
+
+=item previous_balance
+
+previous_balance
+
+=item billing_balance
+
+billing_balance
+
+=item closed
+
+closed
+
+=item statementnum
+
+statementnum
+
+=item agent_invid
+
+agent_invid
+
+=item promised_date
+
+promised_date
+
+=item void_date
+
+void_date
+
+=item reason
+
+reason
+
+=item void_usernum
+
+void_usernum
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new voided invoice.  To add the voided invoice to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_void'; }
+sub notice_name { 'VOIDED Invoice'; }
+#XXXsub template_conf { 'quotation_'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid voided invoice.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('invnum')
+    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
+    || $self->ut_numbern('_date')
+    || $self->ut_money('charged')
+    || $self->ut_textn('invoice_terms')
+    || $self->ut_moneyn('previous_balance')
+    || $self->ut_moneyn('billing_balance')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+    || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum')
+    || $self->ut_numbern('agent_invid')
+    || $self->ut_numbern('promised_date')
+    || $self->ut_numbern('void_date')
+    || $self->ut_textn('reason')
+    || $self->ut_numbern('void_usernum')
+  ;
+  return $error if $error;
+
+  $self->void_date(time) unless $self->void_date;
+
+  $self->void_usernum($FS::CurrentUser::CurrentUser->usernum)
+    unless $self->void_usernum;
+
+  $self->SUPER::check;
+}
+
+=item display_invnum
+
+Returns the displayed invoice number for this invoice: agent_invid if
+cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
+
+=cut
+
+sub display_invnum {
+  my $self = shift;
+  my $conf = $self->conf;
+  if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+    return $self->agent_invid;
+  } else {
+    return $self->invnum;
+  }
+}
+
+=item void_access_user
+
+Returns the voiding employee object (see L<FS::access_user>).
+
+=cut
+
+sub void_access_user {
+  my $self = shift;
+  qsearchs('access_user', { 'usernum' => $self->void_usernum } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 9602941..36c6280 100644 (file)
@@ -42,6 +42,7 @@ use FS::payby;
 use FS::cust_pkg;
 use FS::cust_svc;
 use FS::cust_bill;
+use FS::cust_bill_void;
 use FS::legacy_cust_bill;
 use FS::cust_pay;
 use FS::cust_pay_pending;
@@ -1279,6 +1280,7 @@ sub merge {
 
   tie my %financial_tables, 'Tie::IxHash',
     'cust_bill'      => 'invoices',
+    'cust_bill_void' => 'voided invoices',
     'cust_statement' => 'statements',
     'cust_credit'    => 'credits',
     'cust_pay'       => 'payments',
@@ -3646,6 +3648,20 @@ be passed.
 
 =cut
 
+=item cust_bill_void
+
+Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
+
+=cut
+
+sub cust_bill_void {
+  my $self = shift;
+
+  map { $_ } #return $self->num_cust_bill_void unless wantarray;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
+}
+
 sub cust_statement {
   my $self = shift;
   my $opt = ref($_[0]) ? shift : { @_ };
@@ -3802,7 +3818,7 @@ sub cust_pay_void {
 
 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
 
-Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
+Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
 
 Optionally, a list or hashref of additional arguments to the qsearch call can
 be passed.
diff --git a/FS/FS/cust_tax_exempt_pkg_void.pm b/FS/FS/cust_tax_exempt_pkg_void.pm
new file mode 100644 (file)
index 0000000..51c85b4
--- /dev/null
@@ -0,0 +1,138 @@
+package FS::cust_tax_exempt_pkg_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_main_county;
+
+=head1 NAME
+
+FS::cust_tax_exempt_pkg_void - Object methods for cust_tax_exempt_pkg_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_tax_exempt_pkg_void;
+
+  $record = new FS::cust_tax_exempt_pkg_void \%hash;
+  $record = new FS::cust_tax_exempt_pkg_void { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_tax_exempt_pkg_void object represents a voided record of a customer
+tax exemption.  FS::cust_tax_exempt_pkg_void inherits from FS::Record.  The
+following fields are currently supported:
+
+=over 4
+
+=item exemptpkgnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item taxnum
+
+taxnum
+
+=item year
+
+year
+
+=item month
+
+month
+
+=item creditbillpkgnum
+
+creditbillpkgnum
+
+=item amount
+
+amount
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_tax_exempt_pkg_void'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('exemptpkgnum')
+    || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+    || $self->ut_foreign_key('taxnum', 'cust_main_county', 'taxnum')
+    || $self->ut_number('year')
+    || $self->ut_number('month')
+    || $self->ut_numbern('creditbillpkgnum') #no FK check, will have been del'ed
+    || $self->ut_money('amount')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html
new file mode 100755 (executable)
index 0000000..f2930ec
--- /dev/null
@@ -0,0 +1,22 @@
+%if ( $error ) {
+%  $cgi->param('error', $error);
+<% $cgi->redirect(popurl(1). "void-cust_bill.html?". $cgi->query_string ) %>
+%} else {
+<% $cgi->redirect(popurl(3). "view/cust_main.cgi?". $custnum) %>
+%}
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Void invoices');
+
+#untaint invnum
+$cgi->param('invnum') =~ /^(\d+)$/ || die "Illegal invnum";
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+
+my $custnum = $cust_bill->custnum;
+
+my $error = $cust_bill->void( $cgi->param('reason') );
+
+</%init>
diff --git a/httemplate/misc/void-cust_bill.html b/httemplate/misc/void-cust_bill.html
new file mode 100644 (file)
index 0000000..1608fd0
--- /dev/null
@@ -0,0 +1,45 @@
+<& /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) %>
+<TR>
+  <TD ALIGN="right">Reason</TD>
+  <TD><INPUT TYPE="text" NAME="reason" VALUE="<% $cgi->param('reason') |h %>"></TD>
+</TR>
+
+</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/view/cust_bill_void.html b/httemplate/view/cust_bill_void.html
new file mode 100755 (executable)
index 0000000..c7c5da1
--- /dev/null
@@ -0,0 +1,56 @@
+<& /elements/header.html, mt('Voided Invoice'),  menubar(
+  emt("View this customer (#[_1])",$display_custnum) => "${p}view/cust_main.cgi?$custnum",
+) &>
+
+%#XXX something very big and obvious showing its voided...
+
+% #voided PDFs?
+% #if ( $conf->exists('invoice_latex') ) {
+%#
+%#  <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $link %>"><% mt('View typeset invoice PDF') |h %></A>
+%#  <BR><BR>
+% #} 
+
+% if ( $conf->exists('invoice_html') ) { 
+  <% join('', $cust_bill_void->print_html(\%opt) ) %>
+% } else { 
+  <PRE><% join('', $cust_bill_void->print_text(\%opt) ) %></PRE>
+% } 
+
+<& /elements/footer.html &>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('View invoices');
+
+my $invnum;
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ ) {
+  $invnum = $1;
+} else {
+  $invnum = $cgi->param('invnum');
+}
+
+my $conf = new FS::Conf;
+
+my %opt = (
+  'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+);
+
+my $cust_bill_void = qsearchs({
+  'select'    => 'cust_bill_void.*',
+  'table'     => 'cust_bill_void',
+  #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+  'hashref'   => { 'quotationnum' => $quotationnum },
+  #'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die "Quotation #$quotationnum not found!" unless $quotation;
+
+my $custnum = $cust_bill->custnum;
+my $display_custnum = $cust_bill->cust_main->display_custnum;
+
+#my $link = "invnum=$invnum";
+
+</%init>
index 9e08c0c..166addb 100644 (file)
 %                  ? sprintf("$money_char\%.2f", $item->{'charge'})
 %                  : exists($item->{'charge_nobal'})
 %                    ? sprintf("$money_char\%.2f", $item->{'charge_nobal'})
-%                    : '';
+%                    : exists($item->{'void_charge'})
+%                      ? sprintf("<DEL>$money_char\%.2f</DEL>", $item->{'void_charge'})
+%                      : '';
 %
 %  my $payment = exists($item->{'payment'})
 %                  ? sprintf("-&nbsp;$money_char\%.2f", $item->{'payment'})
@@ -428,6 +430,15 @@ foreach my $cust_bill ($cust_main->cust_bill) {
   $num_cust_bill++;
 }
 
+#voided invoices
+foreach my $cust_bill_void ($cust_main->cust_bill_void) {
+  push @history, {
+    'date'        => $cust_bill_void->_date,
+    'desc'        => include('payment_history/voided_invoice.html', $cust_bill_void, %opt ),
+    'void_charge' => $cust_bill_void->charged,
+  };
+}
+
 #statements
 foreach my $cust_statement ($cust_main->cust_statement) {
   push @history, {
index 3028f0f..96a9f54 100644 (file)
@@ -1,4 +1,4 @@
-<% $link %><% $invoice %><% $link ? '</A>' : '' %><% $delete %><% $under %>
+<% $link %><% $invoice %><% $link ? '</A>' : '' %><% "$void$delete$under" %>
 <%init>
 
 my( $cust_bill, %opt ) = @_;
@@ -26,6 +26,18 @@ my $link = $curuser->access_right('View invoices')
              ? qq!<A HREF="${p}view/cust_bill.cgi?$invnum">!
              : '';
 
+my $void = '';
+if ( $cust_bill->closed !~ /^Y/i && $curuser->access_right('Void invoices') ) {
+  $void =
+    ' ('. include('/elements/popup_link.html',
+                    'label'     => emt('void'),
+                    'action'    => "${p}misc/void-cust_bill.html?;invnum=".
+                                    $cust_bill->invnum,
+                    'actionlabel' => emt('Void Invoice'),
+                 ).
+     ')';
+}
+
 my $delete = '';
 $delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum",
                             emt('Are you sure you want to delete this invoice?'),
diff --git a/httemplate/view/cust_main/payment_history/voided_invoice.html b/httemplate/view/cust_main/payment_history/voided_invoice.html
new file mode 100644 (file)
index 0000000..422edb2
--- /dev/null
@@ -0,0 +1,52 @@
+<DEL><% $link %><% $invoice %><% $link ? '</A>' : '' %></DEL>
+<I><% mt("voided [_1]", time2str($date_format, $cust_bill_void->void_date) ) |h %> 
+% my $void_user = $cust_bill_void->void_access_user;
+% if ($void_user) {
+    by <% $void_user->username %></I>
+% }
+<% "$unvoid$delete$under" %>
+<%init>
+
+my( $cust_bill_void, %opt ) = @_;
+
+my $date_format = $opt{'date_format'} || '%m/%d/%Y';
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my $invoice = emt("Invoice #[_1] (Balance [_2])",$cust_bill_void->display_invnum, $cust_bill_void->charged);
+
+my $under = '';
+
+my $invnum = $cust_bill_void->invnum;
+
+#XXX use cust_bill.cgi or?
+my $link = $curuser->access_right('View invoices')
+             ? qq!<A HREF="${p}view/cust_bill.cgi?$invnum">!
+             : '';
+
+my $unvoid = ''; #XXX unvoid
+
+my $delete = '';
+$delete = areyousure_link("${p}misc/delete-cust_bill.html?$invnum",
+                            emt('Are you sure you want to delete this invoice?'),
+                            emt('Delete this invoice from the database completely'),
+                            emt('delete')
+                        )
+    if ( $opt{'deleteinvoices'} && $curuser->access_right('Delete invoices') );
+
+my $events = '';
+#1.9
+if ( $cust_bill_void->num_cust_event
+     && (    $curuser->access_right('Billing event reports')
+          || $curuser->access_right('View customer billing events')
+        )
+   ) {
+  $under .=
+    qq!<BR><A HREF="${p}search/cust_event.html?invnum=$invnum">( !.
+      emt('View invoice events').' )</A>';
+}
+$under = '<FONT SIZE="-1">'.$under.'</FONT>' if length($under);
+
+</%init>