invoice voiding, RT#18677
authorIvan Kohler <ivan@freeside.biz>
Wed, 1 Aug 2012 20:16:42 +0000 (13:16 -0700)
committerIvan Kohler <ivan@freeside.biz>
Wed, 1 Aug 2012 20:16:42 +0000 (13:16 -0700)
FS/FS/TemplateItem_Mixin.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_discount_void.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_void.pm
FS/FS/cust_bill_void.pm
FS/t/cust_bill_pkg_discount_void.t [new file with mode: 0644]
httemplate/view/cust_bill_void.html
httemplate/view/cust_main/payment_history/voided_invoice.html

diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm
new file mode 100644 (file)
index 0000000..6d7ea26
--- /dev/null
@@ -0,0 +1,317 @@
+package FS::TemplateItem_Mixin;
+
+use strict;
+use vars qw( $DEBUG $me ); # but NOT $conf
+use Carp;
+use FS::UID;
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::part_pkg;
+use FS::cust_pkg;
+
+$DEBUG = 0;
+$me = '[FS::TemplateItem_Mixin]';
+
+=item cust_pkg
+
+Returns the package (see L<FS::cust_pkg>) for this invoice line item.
+
+=cut
+
+sub cust_pkg {
+  my $self = shift;
+  carp "$me $self -> cust_pkg" if $DEBUG;
+  qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
+=item part_pkg
+
+Returns the package definition for this invoice line item.
+
+=cut
+
+sub part_pkg {
+  my $self = shift;
+  if ( $self->pkgpart_override ) {
+    qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
+  } else {
+    my $part_pkg;
+    my $cust_pkg = $self->cust_pkg;
+    $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+    $part_pkg;
+  }
+
+}
+
+=item desc
+
+Returns a description for this line item.  For typical line items, this is the
+I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
+For one-shot line items and named taxes, it is the I<itemdesc> field of this
+line item, and for generic taxes, simply returns "Tax".
+
+=cut
+
+sub desc {
+  my $self = shift;
+
+  if ( $self->pkgnum > 0 ) {
+    $self->itemdesc || $self->part_pkg->pkg;
+  } else {
+    my $desc = $self->itemdesc || 'Tax';
+    $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
+    $desc;
+  }
+}
+
+=item details [ OPTION => VALUE ... ]
+
+Returns an array of detail information for the invoice line item.
+
+Currently available options are: I<format>, I<escape_function> and
+I<format_function>.
+
+If I<format> is set to html or latex then the array members are improved
+for tabular appearance in those environments if possible.
+
+If I<escape_function> is set then the array members are processed by this
+function before being returned.
+
+I<format_function> overrides the normal HTML or LaTeX function for returning
+formatted CDRs.  It can be set to a subroutine which returns an empty list
+to skip usage detail:
+
+  'format_function' => sub { () },
+
+=cut
+
+sub details {
+  my ( $self, %opt ) = @_;
+  my $escape_function = $opt{escape_function} || sub { shift };
+
+  my $csv = new Text::CSV_XS;
+
+  if ( $opt{format_function} ) {
+
+    #this still expects to be passed a cust_bill_pkg_detail object as the
+    #second argument, which is expensive
+    carp "deprecated format_function passed to cust_bill_pkg->details";
+    my $format_sub = $opt{format_function} if $opt{format_function};
+
+    map { ( $_->format eq 'C'
+              ? &{$format_sub}( $_->detail, $_ )
+              : &{$escape_function}( $_->detail )
+          )
+        }
+      qsearch ({ 'table'    => $self->detail_table,
+                 'hashref'  => { 'billpkgnum' => $self->billpkgnum },
+                 'order_by' => 'ORDER BY detailnum',
+              });
+
+  } elsif ( $opt{'no_usage'} ) {
+
+    my $sql = "SELECT detail FROM ". $self->detail_table.
+              "  WHERE billpkgnum = ". $self->billpkgnum.
+              "    AND ( format IS NULL OR format != 'C' ) ".
+              "  ORDER BY detailnum";
+    my $sth = dbh->prepare($sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+
+    map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
+
+  } else {
+
+    my $format_sub;
+    my $format = $opt{format} || '';
+    if ( $format eq 'html' ) {
+
+      $format_sub = sub { my $detail = shift;
+                          $csv->parse($detail) or return "can't parse $detail";
+                          join('</TD><TD>', map { &$escape_function($_) }
+                                            $csv->fields
+                              );
+                        };
+
+    } elsif ( $format eq 'latex' ) {
+
+      $format_sub = sub {
+        my $detail = shift;
+        $csv->parse($detail) or return "can't parse $detail";
+        #join(' & ', map { '\small{'. &$escape_function($_). '}' }
+        #            $csv->fields );
+        my $result = '';
+        my $column = 1;
+        foreach ($csv->fields) {
+          $result .= ' & ' if $column > 1;
+          if ($column > 6) {                     # KLUDGE ALERT!
+            $result .= '\multicolumn{1}{l}{\scriptsize{'.
+                       &$escape_function($_). '}}';
+          }else{
+            $result .= '\scriptsize{'.  &$escape_function($_). '}';
+          }
+          $column++;
+        }
+        $result;
+      };
+
+    } else {
+
+      $format_sub = sub { my $detail = shift;
+                          $csv->parse($detail) or return "can't parse $detail";
+                          join(' - ', map { &$escape_function($_) }
+                                      $csv->fields
+                              );
+                        };
+
+    }
+
+    my $sql = "SELECT format, detail FROM ". $self->detail_table.
+              "  WHERE billpkgnum = ". $self->billpkgnum.
+              "  ORDER BY detailnum";
+    my $sth = dbh->prepare($sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+
+    #avoid the fetchall_arrayref and loop for less memory usage?
+
+    map { (defined($_->[0]) && $_->[0] eq 'C')
+            ? &{$format_sub}(      $_->[1] )
+            : &{$escape_function}( $_->[1] );
+        }
+      @{ $sth->fetchall_arrayref };
+
+  }
+
+}
+
+=item details_header [ OPTION => VALUE ... ]
+
+Returns a list representing an invoice line item detail header, if any.
+This relies on the behavior of voip_cdr in that it expects the header
+to be the first CSV formatted detail (as is expected by invoice generation
+routines).  Returns the empty list otherwise.
+
+=cut
+
+sub details_header {
+  my $self = shift;
+
+  my $csv = new Text::CSV_XS;
+
+  my @detail = 
+    qsearch ({ 'table'    => $self->detail_table,
+               'hashref'  => { 'billpkgnum' => $self->billpkgnum,
+                               'format'     => 'C',
+                             },
+               'order_by' => 'ORDER BY detailnum LIMIT 1',
+            });
+  return() unless scalar(@detail);
+  $csv->parse($detail[0]->detail) or return ();
+  $csv->fields;
+}
+
+=item quantity
+
+=cut
+
+sub quantity {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('quantity', $value);
+  }
+  $self->getfield('quantity') || 1;
+}
+
+=item unitsetup
+
+=cut
+
+sub unitsetup {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('unitsetup', $value);
+  }
+  $self->getfield('unitsetup') eq ''
+    ? $self->getfield('setup')
+    : $self->getfield('unitsetup');
+}
+
+=item unitrecur
+
+=cut
+
+sub unitrecur {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('unitrecur', $value);
+  }
+  $self->getfield('unitrecur') eq ''
+    ? $self->getfield('recur')
+    : $self->getfield('unitrecur');
+}
+
+=item cust_bill_pkg_display [ type => TYPE ]
+
+Returns an array of display information for the invoice line item optionally
+limited to 'TYPE'.
+
+=cut
+
+sub cust_bill_pkg_display {
+  my ( $self, %opt ) = @_;
+
+  my $class = 'FS::'. $self->display_table;
+
+  my $default = $class->new( { billpkgnum =>$self->billpkgnum } );
+
+  my $type = $opt{type} if exists $opt{type};
+  my @result;
+
+  if ( $self->get('display') ) {
+    @result = grep { defined($type) ? ($type eq $_->type) : 1 }
+              @{ $self->get('display') };
+  } else {
+    my $hashref = { 'billpkgnum' => $self->billpkgnum };
+    $hashref->{type} = $type if defined($type);
+    
+    @result = qsearch ({ 'table'    => $self->display_table,
+                         'hashref'  => { 'billpkgnum' => $self->billpkgnum },
+                         'order_by' => 'ORDER BY billpkgdisplaynum',
+                      });
+  }
+
+  push @result, $default unless ( scalar(@result) || $type );
+
+  @result;
+
+}
+
+=item cust_bill_pkg_detail [ CLASSNUM ]
+
+Returns the list of associated cust_bill_pkg_detail objects
+The optional CLASSNUM argument will limit the details to the specified usage
+class.
+
+=cut
+
+sub cust_bill_pkg_detail {
+  my $self = shift;
+  my $classnum = shift || '';
+
+  my %hash = ( 'billpkgnum' => $self->billpkgnum );
+  $hash{classnum} = $classnum if $classnum;
+
+  qsearch( $self->detail_table, \%hash ),
+
+}
+
+=item cust_bill_pkg_discount 
+
+Returns the list of associated cust_bill_pkg_discount objects.
+
+=cut
+
+sub cust_bill_pkg_discount {
+  my $self = shift;
+  qsearch( $self->discount_table, { 'billpkgnum' => $self->billpkgnum } );
+}
+
+1;
index 2ceef04..304d51d 100644 (file)
@@ -1,14 +1,13 @@
 package FS::cust_bill_pkg;
+use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
 
 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;
+use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
-use FS::part_pkg;
 use FS::cust_bill;
 use FS::cust_bill_pkg_detail;
 use FS::cust_bill_pkg_display;
@@ -26,7 +25,6 @@ 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 );
 
 $DEBUG = 0;
 $me = '[FS::cust_bill_pkg]';
@@ -125,6 +123,13 @@ customer object (see L<FS::cust_main>).
 
 sub table { 'cust_bill_pkg'; }
 
+sub detail_table            { 'cust_bill_pkg_detail'; }
+sub display_table           { 'cust_bill_pkg_display'; }
+sub discount_table          { 'cust_bill_pkg_discount'; }
+#sub tax_location_table      { 'cust_bill_pkg_tax_location'; }
+#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
+#sub tax_exempt_pkg_table    { 'cust_tax_exempt_pkg'; }
+
 =item insert
 
 Adds this line item to the database.  If there is an error, returns the error,
@@ -270,6 +275,7 @@ sub void {
   foreach my $table (qw(
     cust_bill_pkg_detail
     cust_bill_pkg_display
+    cust_bill_pkg_discount
     cust_bill_pkg_tax_location
     cust_bill_pkg_tax_rate_location
     cust_tax_exempt_pkg
@@ -326,6 +332,7 @@ sub delete {
   foreach my $table (qw(
     cust_bill_pkg_detail
     cust_bill_pkg_display
+    cust_bill_pkg_discount
     cust_bill_pkg_tax_location
     cust_bill_pkg_tax_rate_location
     cust_tax_exempt_pkg
@@ -462,36 +469,6 @@ sub regularize_details {
   return;
 }
 
-=item cust_pkg
-
-Returns the package (see L<FS::cust_pkg>) for this invoice line item.
-
-=cut
-
-sub cust_pkg {
-  my $self = shift;
-  carp "$me $self -> cust_pkg" if $DEBUG;
-  qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-}
-
-=item part_pkg
-
-Returns the package definition for this invoice line item.
-
-=cut
-
-sub part_pkg {
-  my $self = shift;
-  if ( $self->pkgpart_override ) {
-    qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
-  } else {
-    my $part_pkg;
-    my $cust_pkg = $self->cust_pkg;
-    $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
-    $part_pkg;
-  }
-}
-
 =item cust_bill
 
 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
@@ -521,173 +498,6 @@ sub previous_cust_bill_pkg {
   });
 }
 
-=item details [ OPTION => VALUE ... ]
-
-Returns an array of detail information for the invoice line item.
-
-Currently available options are: I<format>, I<escape_function> and
-I<format_function>.
-
-If I<format> is set to html or latex then the array members are improved
-for tabular appearance in those environments if possible.
-
-If I<escape_function> is set then the array members are processed by this
-function before being returned.
-
-I<format_function> overrides the normal HTML or LaTeX function for returning
-formatted CDRs.  It can be set to a subroutine which returns an empty list
-to skip usage detail:
-
-  'format_function' => sub { () },
-
-=cut
-
-sub details {
-  my ( $self, %opt ) = @_;
-  my $escape_function = $opt{escape_function} || sub { shift };
-
-  my $csv = new Text::CSV_XS;
-
-  if ( $opt{format_function} ) {
-
-    #this still expects to be passed a cust_bill_pkg_detail object as the
-    #second argument, which is expensive
-    carp "deprecated format_function passed to cust_bill_pkg->details";
-    my $format_sub = $opt{format_function} if $opt{format_function};
-
-    map { ( $_->format eq 'C'
-              ? &{$format_sub}( $_->detail, $_ )
-              : &{$escape_function}( $_->detail )
-          )
-        }
-      qsearch ({ 'table'    => 'cust_bill_pkg_detail',
-                 'hashref'  => { 'billpkgnum' => $self->billpkgnum },
-                 'order_by' => 'ORDER BY detailnum',
-              });
-
-  } elsif ( $opt{'no_usage'} ) {
-
-    my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
-              "  WHERE billpkgnum = ". $self->billpkgnum.
-              "    AND ( format IS NULL OR format != 'C' ) ".
-              "  ORDER BY detailnum";
-    my $sth = dbh->prepare($sql) or die dbh->errstr;
-    $sth->execute or die $sth->errstr;
-
-    map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
-
-  } else {
-
-    my $format_sub;
-    my $format = $opt{format} || '';
-    if ( $format eq 'html' ) {
-
-      $format_sub = sub { my $detail = shift;
-                          $csv->parse($detail) or return "can't parse $detail";
-                          join('</TD><TD>', map { &$escape_function($_) }
-                                            $csv->fields
-                              );
-                        };
-
-    } elsif ( $format eq 'latex' ) {
-
-      $format_sub = sub {
-        my $detail = shift;
-        $csv->parse($detail) or return "can't parse $detail";
-        #join(' & ', map { '\small{'. &$escape_function($_). '}' }
-        #            $csv->fields );
-        my $result = '';
-        my $column = 1;
-        foreach ($csv->fields) {
-          $result .= ' & ' if $column > 1;
-          if ($column > 6) {                     # KLUDGE ALERT!
-            $result .= '\multicolumn{1}{l}{\scriptsize{'.
-                       &$escape_function($_). '}}';
-          }else{
-            $result .= '\scriptsize{'.  &$escape_function($_). '}';
-          }
-          $column++;
-        }
-        $result;
-      };
-
-    } else {
-
-      $format_sub = sub { my $detail = shift;
-                          $csv->parse($detail) or return "can't parse $detail";
-                          join(' - ', map { &$escape_function($_) }
-                                      $csv->fields
-                              );
-                        };
-
-    }
-
-    my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ".
-              "  WHERE billpkgnum = ". $self->billpkgnum.
-              "  ORDER BY detailnum";
-    my $sth = dbh->prepare($sql) or die dbh->errstr;
-    $sth->execute or die $sth->errstr;
-
-    #avoid the fetchall_arrayref and loop for less memory usage?
-
-    map { (defined($_->[0]) && $_->[0] eq 'C')
-            ? &{$format_sub}(      $_->[1] )
-            : &{$escape_function}( $_->[1] );
-        }
-      @{ $sth->fetchall_arrayref };
-
-  }
-
-}
-
-=item details_header [ OPTION => VALUE ... ]
-
-Returns a list representing an invoice line item detail header, if any.
-This relies on the behavior of voip_cdr in that it expects the header
-to be the first CSV formatted detail (as is expected by invoice generation
-routines).  Returns the empty list otherwise.
-
-=cut
-
-sub details_header {
-  my $self = shift;
-  return '' unless defined dbdef->table('cust_bill_pkg_detail');
-
-  my $csv = new Text::CSV_XS;
-
-  my @detail = 
-    qsearch ({ 'table'    => 'cust_bill_pkg_detail',
-               'hashref'  => { 'billpkgnum' => $self->billpkgnum,
-                               'format'     => 'C',
-                             },
-               'order_by' => 'ORDER BY detailnum LIMIT 1',
-            });
-  return() unless scalar(@detail);
-  $csv->parse($detail[0]->detail) or return ();
-  $csv->fields;
-}
-
-=item desc
-
-Returns a description for this line item.  For typical line items, this is the
-I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
-For one-shot line items and named taxes, it is the I<itemdesc> field of this
-line item, and for generic taxes, simply returns "Tax".
-
-=cut
-
-sub desc {
-  my $self = shift;
-
-  if ( $self->pkgnum > 0 ) {
-    $self->itemdesc || $self->part_pkg->pkg;
-  } else {
-    my $desc = $self->itemdesc || 'Tax';
-    $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
-    $desc;
-  }
-}
-
 =item owed_setup
 
 Returns the amount owed (still outstanding) on this line item's setup fee,
@@ -765,45 +575,6 @@ sub units {
   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
 }
 
-=item quantity
-
-=cut
-
-sub quantity {
-  my( $self, $value ) = @_;
-  if ( defined($value) ) {
-    $self->setfield('quantity', $value);
-  }
-  $self->getfield('quantity') || 1;
-}
-
-=item unitsetup
-
-=cut
-
-sub unitsetup {
-  my( $self, $value ) = @_;
-  if ( defined($value) ) {
-    $self->setfield('unitsetup', $value);
-  }
-  $self->getfield('unitsetup') eq ''
-    ? $self->getfield('setup')
-    : $self->getfield('unitsetup');
-}
-
-=item unitrecur
-
-=cut
-
-sub unitrecur {
-  my( $self, $value ) = @_;
-  if ( defined($value) ) {
-    $self->setfield('unitrecur', $value);
-  }
-  $self->getfield('unitrecur') eq ''
-    ? $self->getfield('recur')
-    : $self->getfield('unitrecur');
-}
 
 =item set_display OPTION => VALUE ...
 
@@ -1015,44 +786,8 @@ sub usage_classes {
 
 }
 
-=item cust_bill_pkg_display [ type => TYPE ]
-
-Returns an array of display information for the invoice line item optionally
-limited to 'TYPE'.
-
-=cut
-
-sub cust_bill_pkg_display {
-  my ( $self, %opt ) = @_;
-
-  my $default =
-    new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
-
-  my $type = $opt{type} if exists $opt{type};
-  my @result;
-
-  if ( $self->get('display') ) {
-    @result = grep { defined($type) ? ($type eq $_->type) : 1 }
-              @{ $self->get('display') };
-  } else {
-    my $hashref = { 'billpkgnum' => $self->billpkgnum };
-    $hashref->{type} = $type if defined($type);
-    
-    @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
-                         'hashref'  => { 'billpkgnum' => $self->billpkgnum },
-                         'order_by' => 'ORDER BY billpkgdisplaynum',
-                      });
-  }
-
-  push @result, $default unless ( scalar(@result) || $type );
-
-  @result;
-
-}
-
 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
 # and FS::cust_main::bill
-
 sub _cust_tax_exempt_pkg {
   my ( $self ) = @_;
 
@@ -1080,36 +815,6 @@ sub cust_bill_pkg_tax_Xlocation {
 
 }
 
-=item cust_bill_pkg_detail [ CLASSNUM ]
-
-Returns the list of associated cust_bill_pkg_detail objects
-The optional CLASSNUM argument will limit the details to the specified usage
-class.
-
-=cut
-
-sub cust_bill_pkg_detail {
-  my $self = shift;
-  my $classnum = shift || '';
-
-  my %hash = ( 'billpkgnum' => $self->billpkgnum );
-  $hash{classnum} = $classnum if $classnum;
-
-  qsearch( 'cust_bill_pkg_detail', \%hash ),
-
-}
-
-=item cust_bill_pkg_discount 
-
-Returns the list of associated cust_bill_pkg_discount objects.
-
-=cut
-
-sub cust_bill_pkg_discount {
-  my $self = shift;
-  qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
-}
-
 =item recur_show_zero
 
 =cut
diff --git a/FS/FS/cust_bill_pkg_discount_void.pm b/FS/FS/cust_bill_pkg_discount_void.pm
new file mode 100644 (file)
index 0000000..859ef3c
--- /dev/null
@@ -0,0 +1,129 @@
+package FS::cust_bill_pkg_discount_void;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record; # qw( qsearch qsearchs );
+use FS::cust_bill_pkg_void;
+use FS::cust_pkg_discount;
+
+=head1 NAME
+
+FS::cust_bill_pkg_discount_void - Object methods for cust_bill_pkg_discount_void records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_discount_void;
+
+  $record = new FS::cust_bill_pkg_discount_void \%hash;
+  $record = new FS::cust_bill_pkg_discount_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_discount_void object represents the slice of a customer
+discount applied to a specific voided line item.
+FS::cust_bill_pkg_discount_void inherits from FS::Record.  The following fields
+are currently supported:
+
+=over 4
+
+=item billpkgdiscountnum
+
+primary key
+
+=item billpkgnum
+
+billpkgnum
+
+=item pkgdiscountnum
+
+pkgdiscountnum
+
+=item amount
+
+amount
+
+=item months
+
+months
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_bill_pkg_discount_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 example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('billpkgdiscountnum')
+    || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg_void', 'billpkgnum' )
+    || $self->ut_foreign_key('pkgdiscountnum', 'cust_pkg_discount', 'pkgdiscountnum' )
+    || $self->ut_money('amount')
+    || $self->ut_float('months')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 1982839..7855d58 100644 (file)
@@ -1,8 +1,12 @@
 package FS::cust_bill_pkg_void;
+use base qw( FS::TemplateItem_Mixin FS::Record );
 
 use strict;
-use base qw( FS::Record );
-use FS::Record; # qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_bill_void;
+use FS::cust_bill_pkg_detail_void;
+use FS::cust_bill_pkg_display_void;
+use FS::cust_bill_pkg_discount_void;
 
 =head1 NAME
 
@@ -113,6 +117,13 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'cust_bill_pkg_void'; }
 
+sub detail_table            { 'cust_bill_pkg_detail_void'; }
+sub display_table           { 'cust_bill_pkg_display_void'; }
+sub discount_table          { 'cust_bill_pkg_discount_void'; }
+#sub tax_location_table      { 'cust_bill_pkg_tax_location'; }
+#sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
+#sub tax_exempt_pkg_table    { 'cust_tax_exempt_pkg'; }
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
@@ -147,7 +158,7 @@ sub check {
   my $error = 
     $self->ut_number('billpkgnum')
     || $self->ut_snumber('pkgnum')
-    || $self->ut_number('invnum') #cust_bill or cust_bill_void ?
+    || $self->ut_number('invnum') #cust_bill or cust_bill_void, if we ever support line item voiding
     || $self->ut_numbern('pkgpart_override')
     || $self->ut_money('setup')
     || $self->ut_money('recur')
@@ -167,6 +178,19 @@ sub check {
   $self->SUPER::check;
 }
 
+=item cust_bill
+
+Returns the voided invoice (see L<FS::cust_bill_void>) for this voided line
+item.
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  #cust_bill or cust_bill_void, if we ever support line item voiding
+  qsearchs( 'cust_bill_void', { 'invnum' => $self->invnum } );
+}
+
 =back
 
 =head1 BUGS
index c782172..cd6a9e1 100644 (file)
@@ -2,10 +2,11 @@ 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::Record qw( qsearch qsearchs );
 use FS::cust_main;
 use FS::cust_statement;
 use FS::access_user;
+use FS::cust_bill_pkg_void;
 
 =head1 NAME
 
@@ -203,6 +204,33 @@ sub void_access_user {
   qsearchs('access_user', { 'usernum' => $self->void_usernum } );
 }
 
+=item cust_main
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs('cust_main', { 'custnum' => $self->custnum } );
+}
+
+=item cust_bill_pkg
+
+=cut
+
+sub cust_bill_pkg { #actually cust_bill_pkg_void objects
+  my $self = shift;
+  qsearch('cust_bill_pkg_void', { invnum=>$self->invnum });
+}
+
+=back
+
+=item enable_previous
+
+=cut
+
+sub enable_previous { 0 }
+
+
 =back
 
 =head1 BUGS
diff --git a/FS/t/cust_bill_pkg_discount_void.t b/FS/t/cust_bill_pkg_discount_void.t
new file mode 100644 (file)
index 0000000..e591eb0
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_discount_void;
+$loaded=1;
+print "ok 1\n";
index c7c5da1..148c0ed 100755 (executable)
 %#  <BR><BR>
 % #} 
 
+<DIV STYLE="color:#FF0000; font-size:1000%; font-weight:bold; z-index:100;
+            position: absolute; top: 300px; left: 130px;
+            zoom: 1; filter: alpha(opacity=25); opacity: 0.25;
+">VOID</DIV>
+
 % if ( $conf->exists('invoice_html') ) { 
   <% join('', $cust_bill_void->print_html(\%opt) ) %>
 % } else { 
@@ -43,13 +48,13 @@ my $cust_bill_void = qsearchs({
   'select'    => 'cust_bill_void.*',
   'table'     => 'cust_bill_void',
   #'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
-  'hashref'   => { 'quotationnum' => $quotationnum },
+  'hashref'   => { 'invnum' => $invnum },
   #'extra_sql' => ' AND '. $curuser->agentnums_sql,
 });
-die "Quotation #$quotationnum not found!" unless $quotation;
+die "Voided invoice #$invnum not found!" unless $cust_bill_void;
 
-my $custnum = $cust_bill->custnum;
-my $display_custnum = $cust_bill->cust_main->display_custnum;
+my $custnum = $cust_bill_void->custnum;
+my $display_custnum = $cust_bill_void->cust_main->display_custnum;
 
 #my $link = "invnum=$invnum";
 
index 422edb2..7bf2063 100644 (file)
@@ -21,9 +21,8 @@ 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">!
+             ? qq!<A HREF="${p}view/cust_bill_void.html?$invnum">!
              : '';
 
 my $unvoid = ''; #XXX unvoid