tax adjustments, RT#5595
authorivan <ivan>
Thu, 25 Jun 2009 01:28:53 +0000 (01:28 +0000)
committerivan <ivan>
Thu, 25 Jun 2009 01:28:53 +0000 (01:28 +0000)
14 files changed:
FS/FS.pm
FS/FS/AccessRight.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_main.pm
FS/FS/cust_tax_adjustment.pm [new file with mode: 0644]
FS/MANIFEST
FS/t/cust_tax_adjustment.t [new file with mode: 0644]
httemplate/edit/cust_tax_adjustment.html [new file with mode: 0644]
httemplate/edit/process/cust_tax_adjustment.html [new file with mode: 0644]
httemplate/search/cust_tax_adjustment.html [new file with mode: 0644]
httemplate/view/cust_main/payment_history.html

index e34b45d..1477e98 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -102,6 +102,8 @@ L<FS::cust_main_county> - Locale (tax rate) class
 
 L<FS::cust_tax_exempt> - Tax exemption record class
 
+L<FS::cust_tax_adjustment> - Tax adjustment record class
+
 L<FS::cust_tax_exempt_pkg> - Line-item specific tax exemption record class
 
 L<FS::svc_Common> - Service base class
index 146b9fa..3157d5f 100644 (file)
@@ -152,6 +152,7 @@ tie my %rights, 'Tie::IxHash',
     'View invoices',
     'Resend invoices', #NEWNEW
     'View customer tax exemptions', #yow
+    'Add customer tax adjustment', #new, but no need to phase in
     'View customer batched payments', #NEW
     'View customer pending payments', #NEW
     'Edit customer pending payments', #NEW
index a9b891c..ac479da 100644 (file)
@@ -2851,6 +2851,13 @@ worry that config_items is freeside-specific and icky.
     ],
   },
 
+  {
+    'key'         => 'enable_tax_adjustments',
+    'section'     => 'billing',
+    'description' => 'Enable the ability to add manual tax adjustments.',
+    'type'        => 'checkbox',
+  },
+
 );
 
 1;
index 0ca15a6..aed8d60 100644 (file)
@@ -513,6 +513,7 @@ sub tables_hashref {
         'sdate',               @date_type,              '', '', 
         'edate',               @date_type,              '', '', 
         'itemdesc',         'varchar', 'NULL', $char_d, '', '', 
+        'itemcomment',      'varchar', 'NULL', $char_d, '', '', 
         'section',          'varchar', 'NULL', $char_d, '', '', 
         'quantity',             'int', 'NULL',      '', '', '',
         'unitsetup',           @money_typen,            '', '', 
@@ -520,7 +521,7 @@ sub tables_hashref {
       ],
       'primary_key' => 'billpkgnum',
       'unique' => [],
-      'index' => [ ['invnum'], [ 'pkgnum' ] ],
+      'index' => [ ['invnum'], [ 'pkgnum' ], [ 'itemdesc' ], ],
     },
 
     'cust_bill_pkg_detail' => {
@@ -800,6 +801,21 @@ sub tables_hashref {
       'index'       => [ [ 'custnum' ] ],
     },
 
+    'cust_tax_adjustment' => {
+      'columns' => [
+        'adjustmentnum', 'serial',     '',      '', '', '',
+        'custnum',          'int',     '',      '', '', '',
+        'taxname',      'varchar',     '', $char_d, '', '',
+        'amount',    @money_type,                   '', '', 
+        'comment',     'varchar',  'NULL', $char_d, '', '', 
+        'billpkgnum',       'int', 'NULL',      '', '', '',
+        #more?  no cust_bill_pkg_tax_location?
+      ],
+      'primary_key' => 'adjustmentnum',
+      'unique'      => [],
+      'index'       => [ [ 'custnum' ], [ 'billpkgnum' ] ],
+    },
+
     'cust_main_county' => { #county+state+country are checked off the
                             #cust_main_county for validation and to provide
                             # a tax rate.
index 737f68c..17b88c5 100644 (file)
@@ -1457,11 +1457,9 @@ sub print_csv {
   
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
-        my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
-                         ? ( $cust_bill_pkg->itemdesc || 'Tax' )
-                         : 'Tax';
-        ($pkg, $setup, $recur, $sdate, $edate) =
-          ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+        $pkg = $cust_bill_pkg->desc;
+        $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
+        ( $sdate, $edate ) = ( '', '' );
       }
   
       $csv->combine(
index bb07173..fbc67c5 100644 (file)
@@ -175,6 +175,17 @@ sub insert {
     }
   }
 
+  my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
+  if ( $cust_tax_adjustment ) {
+    $cust_tax_adjustment->billpkgnum($self->billpkgnum);
+    $error = $cust_tax_adjustment->replace;
+    warn $error;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
@@ -224,6 +235,7 @@ sub check {
       || $self->ut_numbern('sdate')
       || $self->ut_numbern('edate')
       || $self->ut_textn('itemdesc')
+      || $self->ut_textn('itemcomment')
   ;
   return $error if $error;
 
@@ -381,7 +393,9 @@ sub desc {
   if ( $self->pkgnum > 0 ) {
     $self->itemdesc || $self->part_pkg->pkg;
   } else {
-    $self->itemdesc || 'Tax';
+    my $desc = $self->itemdesc || 'Tax';
+    $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
+    $desc;
   }
 }
 
index e7cdd21..dd99eda 100644 (file)
@@ -41,6 +41,7 @@ use FS::part_referral;
 use FS::cust_main_county;
 use FS::cust_location;
 use FS::cust_main_exemption;
+use FS::cust_tax_adjustment;
 use FS::tax_rate;
 use FS::tax_rate_location;
 use FS::cust_tax_location;
@@ -2661,6 +2662,35 @@ sub bill {
 
   }
 
+  #add tax adjustments
+  warn "adding tax adjustments...\n" if $DEBUG > 2;
+  foreach my $cust_tax_adjustment (
+    qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
+                                     'billpkgnum' => '',
+                                   }
+           )
+  ) {
+
+    my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
+    $total_setup = sprintf('%.2f', $total_setup+$tax );
+
+    my $itemdesc = $cust_tax_adjustment->taxname;
+    $itemdesc = '' if $itemdesc eq 'Tax';
+
+    push @cust_bill_pkg, new FS::cust_bill_pkg {
+      'pkgnum'      => 0,
+      'setup'       => $tax,
+      'recur'       => 0,
+      'sdate'       => '',
+      'edate'       => '',
+      'itemdesc'    => $itemdesc,
+      'itemcomment' => $cust_tax_adjustment->comment,
+      'cust_tax_adjustment' => $cust_tax_adjustment,
+      #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+    };
+
+  }
+
   my $charged = sprintf('%.2f', $total_setup + $total_recur );
 
   #create the new invoice
diff --git a/FS/FS/cust_tax_adjustment.pm b/FS/FS/cust_tax_adjustment.pm
new file mode 100644 (file)
index 0000000..5891368
--- /dev/null
@@ -0,0 +1,149 @@
+package FS::cust_tax_adjustment;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main;
+use FS::cust_bill_pkg;
+
+=head1 NAME
+
+FS::cust_tax_adjustment - Object methods for cust_tax_adjustment records
+
+=head1 SYNOPSIS
+
+  use FS::cust_tax_adjustment;
+
+  $record = new FS::cust_tax_adjustment \%hash;
+  $record = new FS::cust_tax_adjustment { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_tax_adjustment object represents an taxation adjustment.
+FS::cust_tax_adjustment inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item adjustmentnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item taxname
+
+taxname
+
+=item amount
+
+amount
+
+=item comment
+
+comment
+
+=item billpkgnum
+
+billpkgnum
+
+
+=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
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_tax_adjustment'; }
+
+=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 record.  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_numbern('adjustmentnum')
+    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
+    || $self->ut_text('taxname')
+    || $self->ut_money('amount')
+    || $self->ut_textn('comment')
+    || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg', 'billpkgnum' )
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+sub cust_bill_pkg {
+  my $self = shift;
+  qsearchs('cust_bill_pkg', { 'billpkgnum' => $self->billpkgnum } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index b52d7b3..cc4f98e 100644 (file)
@@ -442,3 +442,5 @@ FS/cust_recon.pm
 t/cust_recon.t
 FS/cust_main_exemption.pm
 t/cust_main_exemption.t
+FS/cust_tax_adjustment.pm
+t/cust_tax_adjustment.t
diff --git a/FS/t/cust_tax_adjustment.t b/FS/t/cust_tax_adjustment.t
new file mode 100644 (file)
index 0000000..cc5719a
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_tax_adjustment;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/edit/cust_tax_adjustment.html b/httemplate/edit/cust_tax_adjustment.html
new file mode 100644 (file)
index 0000000..9d4afbc
--- /dev/null
@@ -0,0 +1,102 @@
+<% include('/elements/header-popup.html', 'Tax adjustment' ) %>
+
+<% include('/elements/error.html') %>
+
+<SCRIPT TYPE="text/javascript">
+
+function enable_tax_adjustment () {
+  if (    document.TaxAdjustmentForm.amount.value
+       && document.TaxAdjustmentForm.taxname.selectedIndex > 0  ) {
+    document.TaxAdjustmentForm.submit.disabled = false;
+  } else {
+    document.TaxAdjustmentForm.submit.disabled = true;
+  }
+}
+
+function validate_tax_adjustment () {
+  var comment = document.TaxAdjustmentForm.comment.value;
+  var comment_regex = /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]*)$/ ;
+  var amount = document.TaxAdjustmentForm.amount.value;
+  var amount_regex = /^\s*\$?\s*(\d*(\.?\d{1,2}))\s*$/ ;
+  var rval = true;
+
+  if ( ! amount_regex.test(amount) ) {
+    alert('Illegal amount - enter the amount of the tax adjustment, for example, "5" or "43" or "21.46".');
+    return false;
+  }
+  if ( ! comment_regex.test(comment) ) {
+    alert('Illegal comment - spaces, letters, numbers, and the following punctuation characters are allowed: . , ! ? @ # $ % & ( ) - + ; : ' + "'" + ' " = [ ]' );
+    return false;
+  }
+
+  return true;
+}
+
+</SCRIPT>
+
+<FORM ACTION="process/cust_tax_adjustment.html" NAME="TaxAdjustmentForm" ID="TaxAdjustmentForm" METHOD="POST" onsubmit="document.TaxAdjustmentForm.submit.disabled=true;return validate_tax_adjustment();">
+
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
+
+<TABLE ID="TaxAdjustmentTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 STYLE="background-color: #cccccc">
+
+<TR>
+  <TD ALIGN="right">Tax </TD>
+  <TD>
+    <SELECT NAME="taxname" ID="taxname" onChange="enable_tax_adjustment()" onKeyPress="enable_tax_adjustment()">
+      <OPTION VALUE=""></OPTION>
+%     foreach my $taxname (@taxname) {
+        <OPTION VALUE="<% $taxname %>"><% $taxname %></OPTION>
+%     }
+    </SELECT>
+  </TD>
+</TR>
+
+<TR>
+  <TD ALIGN="right">Amount </TD>
+  <TD>
+    $<INPUT TYPE="text" NAME="amount" SIZE=6 VALUE="<% $amount %>" onChange="enable_tax_adjustment()" onKeyPress="enable_tax_adjustment()">
+  </TD>
+</TR>
+
+<TR>
+  <TD ALIGN="right">Comment </TD>
+  <TD>
+    <INPUT TYPE="text" NAME="comment" SIZE="50" MAXLENGTH="50" VALUE="<% $comment %>" onChange="enable_tax_adjustment()" onKeyPress="enable_tax_adjustment()">
+  </TD>
+</TR>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" ID="submit" NAME="submit" VALUE="Add tax adjustment" <% $cgi->param('error') ? '' :' DISABLED' %>>
+
+</FORM>
+
+</BODY>
+</HTML>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Add customer tax adjustment');
+
+my $sql = 'SELECT DISTINCT(taxname) FROM cust_main_county';
+my $sth = dbh->prepare($sql) or die dbh->errstr;
+$sth->execute() or die $sth->errstr;
+my @taxname = map { $_->[0] || 'Tax' } @{ $sth->fetchall_arrayref([]) };
+
+my $conf = new FS::Conf;
+
+$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum';
+my $custnum = $1;
+
+my $amount = '';
+if ( $cgi->param('amount') =~ /^\s*\$?\s*(\d+(\.\d{1,2})?)\s*$/ ) {
+  $amount = $1;
+}
+
+$cgi->param('comment') =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]]*)$/ 
+  or die 'illegal description';
+my $comment = $1;
+
+</%init>
diff --git a/httemplate/edit/process/cust_tax_adjustment.html b/httemplate/edit/process/cust_tax_adjustment.html
new file mode 100644 (file)
index 0000000..204b5b9
--- /dev/null
@@ -0,0 +1,41 @@
+% if ( $error ) {
+%   $cgi->param('error', $error );
+<% $cgi->redirect($p.'cust_tax_adjustment.html?'. $cgi->query_string) %>
+% } else {
+<% header("Tax adjustment added") %>
+  <SCRIPT TYPE="text/javascript">
+    //window.top.location.reload();
+    parent.cClick();
+  </SCRIPT>
+  </BODY></HTML>
+% }
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Add customer tax adjustment');
+
+my $error = '';
+my $conf = new FS::conf;
+my $param = $cgi->Vars;
+
+$param->{"custnum"} =~ /^(\d+)$/
+  or $error .= "Illegal customer number " . $param->{"custnum"} . "  ";
+my $custnum = $1;
+
+$param->{"amount"} =~ /^\s*(\d*(?:\.?\d{1,2}))\s*$/
+  or $error .= "Illegal amount " . $param->{"amount"} . "  ";
+my $amount = $1;
+
+unless ( $error ) {
+
+  my $cust_tax_adjustment = new FS::cust_tax_adjustment {
+    'custnum' => $custnum,
+    'taxname' => $param->{'taxname'},
+    'amount'  => $amount,
+    'comment' => $param->{'comment'},
+  };
+  $error = $cust_tax_adjustment->insert;
+
+}
+
+</%init>
diff --git a/httemplate/search/cust_tax_adjustment.html b/httemplate/search/cust_tax_adjustment.html
new file mode 100644 (file)
index 0000000..dfc638e
--- /dev/null
@@ -0,0 +1,52 @@
+<% include( 'elements/search.html',
+              'title'         => $title,
+              'name_singular' => 'tax adjustment',
+              'query'         => $query,
+              'count_query'   => $count_query,
+              'header'        => [ 'Tax', 'Amount', 'Comment', 'Invoice' ],
+              'fields'        => [ 'taxname',
+                                   sub { $money_char. shift->amount },
+                                   'comment',
+                                   sub { my $l = shift->cust_bill_pkg;
+                                         $l ? '#'.$l->invnum : '';
+                                       },
+                                 ],
+              'links'         => [ '', '', '', $ilink ],
+          )
+%>
+
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Add customer tax adjustment');
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+
+my $count_query = 'SELECT COUNT(*) FROM cust_tax_adjustment';
+
+my $hashref = {};
+
+my $custnum = $cgi->param('custnum');
+my $cust_main;
+if ( $custnum ) {
+  $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+  $hashref->{'custnum'} = $custnum;
+}
+
+my $title = 'Tax adjustments';
+$title .= ' for '. $cust_main->name if $cust_main;
+
+my $query = { 'table'   => 'cust_tax_adjustment',
+              'hashref' => $hashref,
+            };
+
+my $ilink = [ $p.'view/cust_bill.cgi?', sub { my $l = shift->cust_bill_pkg;
+                                               $l ? $l->invnum : 'EXCEPTION';
+                                             }
+           ];
+
+#XXX would be nice to list customer fields on the report too, if we ever need
+# to link to here without a custnum (i'm sure we will, eventually...)
+
+</%init>
index f2abe0e..1711e14 100644 (file)
 
 %# tax exemption link
 
-% if ( $curuser->access_right('View customer tax exemptions') ) { 
-  <A HREF="<% $p %>search/cust_tax_exempt_pkg.cgi?custnum=<% $custnum %>">View tax exemptions</A>
+% my $view_exemptions = $curuser->access_right('View customer tax exemptions');
+% my $add_adjustment = ( $conf->exists('enable_tax_adjustments')
+%                       && $curuser->access_right('Add customer tax adjustment')
+%                      );
+% if ( $view_exemptions || $add_adjustment ) {
+
+%   if ( $view_exemptions ) {
+      <A HREF="<% $p %>search/cust_tax_exempt_pkg.cgi?custnum=<% $custnum %>">View tax exemptions</A>
+      <% $add_adjustment ? '|' : '' %>
+%   } 
+
+%   if ( $add_adjustment ) {
+      <% include('/elements/popup_link.html', {
+           'action' => $p.'edit/cust_tax_adjustment.html?custnum='. $cust_main->custnum,
+           'label'  => 'Add tax adjustment',
+           'actionlabel' => 'Add tax adjustment',
+           #'color'  => '#333399',
+           #'width' => 763,
+           'height' => 200,
+         })
+      %>
+      |
+      <A HREF="<% $p %>search/cust_tax_adjustment.html?custnum=<% $custnum %>">View tax adjustments</A>
+%   } 
+
   <BR>
-% } 
+% }
 
 %# batched payment links