manual control of quick payment application, #15861
authorMark Wells <mark@freeside.biz>
Tue, 24 Apr 2012 03:27:17 +0000 (20:27 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 24 Apr 2012 03:27:17 +0000 (20:27 -0700)
FS/FS/cust_pay.pm
httemplate/elements/customer-table.html
httemplate/misc/batch-cust_pay.html
httemplate/misc/process/batch-cust_pay.cgi
httemplate/misc/xmlhttp-cust_bill-search.html [new file with mode: 0644]

index ef30809..e15bf01 100644 (file)
@@ -760,6 +760,12 @@ objects.  Returns a list, each element representing the status of inserting the
 corresponding payment - empty.  If there is an error inserting any payment, the
 entire transaction is rolled back, i.e. all payments are inserted or none are.
 
 corresponding payment - empty.  If there is an error inserting any payment, the
 entire transaction is rolled back, i.e. all payments are inserted or none are.
 
+FS::cust_pay objects may have the pseudo-field 'apply_to', containing a 
+reference to an array of (uninserted) FS::cust_bill_pay objects.  If so,
+those objects will be inserted with the paynum of the payment, and for 
+each one, an error message or an empty string will be inserted into the 
+list of errors.
+
 For example:
 
   my @errors = FS::cust_pay->batch_insert(@cust_pay);
 For example:
 
   my @errors = FS::cust_pay->batch_insert(@cust_pay);
@@ -786,19 +792,35 @@ sub batch_insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $errors = 0;
+  my $num_errors = 0;
   
   
-  my @errors = map {
-    my $error = $_->insert( 'manual' => 1 );
-    if ( $error ) { 
-      $errors++;
-    } else {
-      $_->cust_main->apply_payments;
+  my @errors;
+  foreach my $cust_pay (@_) {
+    my $error = $cust_pay->insert( 'manual' => 1 );
+    push @errors, $error;
+    $num_errors++ if $error;
+
+    if ( ref($cust_pay->get('apply_to')) eq 'ARRAY' ) {
+
+      foreach my $cust_bill_pay ( @{ $cust_pay->apply_to } ) {
+        if ( $error ) { # insert placeholders if cust_pay wasn't inserted
+          push @errors, '';
+        }
+        else {
+          $cust_bill_pay->set('paynum', $cust_pay->paynum);
+          my $apply_error = $cust_bill_pay->insert;
+          push @errors, $apply_error || '';
+          $num_errors++ if $apply_error;
+        }
+      }
+
+    } elsif ( !$error ) { #normal case: apply payments as usual
+      $cust_pay->cust_main->apply_payments;
     }
     }
-    $error;
-  } @_;
 
 
-  if ( $errors ) {
+  }
+
+  if ( $num_errors ) {
     $dbh->rollback if $oldAutoCommit;
   } else {
     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
     $dbh->rollback if $oldAutoCommit;
   } else {
     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
index a517ece..b49bf02 100644 (file)
@@ -356,7 +356,7 @@ Example:
 % my $row = 0;
 % for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) { 
 
 % my $row = 0;
 % for ( $row = 0; exists($param->{"custnum$row"}); $row++ ) { 
 
-    <TR>
+    <TR id="row<%$row%>" rownum="<%$row%>">
       <TD>
        <INPUT TYPE      = "text"
                NAME      = "invnum<% $row %>"
       <TD>
        <INPUT TYPE      = "text"
                NAME      = "invnum<% $row %>"
@@ -458,19 +458,24 @@ Example:
 %     my $color = $opt{color}->[$col];
 %     my $font = $color ? qq(<FONT COLOR="$color">) : '';
 %     my $onchange = '';
 %     my $color = $opt{color}->[$col];
 %     my $font = $color ? qq(<FONT COLOR="$color">) : '';
 %     my $onchange = '';
-%     if ( $opt{footer}->[$col] eq '_TOTAL' ) {
+%     if ( $opt{onchange}->[$col] ) {
+%       $onchange = 'onchange="'.$opt{onchange}->[$col].'"';
+%     }
+%     elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
 %       $total[$col] += $value;
 %       $onchange = $opt{prefix}. "calc_total$col();";
 %       $onchange = qq(onchange="$onchange" onkeyup="$onchange");
 %     }
       <TD ALIGN="<% $align %>">
 %       $total[$col] += $value;
 %       $onchange = $opt{prefix}. "calc_total$col();";
 %       $onchange = qq(onchange="$onchange" onkeyup="$onchange");
 %     }
       <TD ALIGN="<% $align %>">
-%     if (! $types->[$col] || $types->[$col] eq 'text') {
-        <INPUT TYPE  = "text"
+%     my $type = $types->[$col] || 'text';
+%     if ($type eq 'text' or $type eq 'checkbox') {
+        <INPUT TYPE  = "<% $type %>"
                NAME  = "<% $name %>"
                ID    = "<% $name %>"
                SIZE  = "<% $size %>"
                STYLE = "text-align: <% $align %>;"
                VALUE = "<% $value %>"
                NAME  = "<% $name %>"
                ID    = "<% $name %>"
                SIZE  = "<% $size %>"
                STYLE = "text-align: <% $align %>;"
                VALUE = "<% $value %>"
+               rownum    = "<% $row %>"
                <% $onchange %>
         >
 %     } elsif ($types->[$col] eq 'immutable') {
                <% $onchange %>
         >
 %     } elsif ($types->[$col] eq 'immutable') {
@@ -485,7 +490,7 @@ Example:
     </TR>
 % } 
 
     </TR>
 % } 
 
-<TR>
+<TR id="row_total">
   <TH COLSPAN=5 ID="<% $opt{'prefix'} %>_TOTAL_TOTAL">
     Total <% $row ? $row-1 : 0 %>
     <% PL($opt{name_singular} || 'customer', ( $row ? $row-1 : 0 ) ) %>
   <TH COLSPAN=5 ID="<% $opt{'prefix'} %>_TOTAL_TOTAL">
     Total <% $row ? $row-1 : 0 %>
     <% PL($opt{name_singular} || 'customer', ( $row ? $row-1 : 0 ) ) %>
@@ -559,7 +564,8 @@ Example:
     var table = document.getElementById('<% $opt{prefix} %>OneTrueTable');
     var tablebody = table.getElementsByTagName('tbody').item(0);
 
     var table = document.getElementById('<% $opt{prefix} %>OneTrueTable');
     var tablebody = table.getElementsByTagName('tbody').item(0);
 
-    var row = table.insertRow(rownum+1);
+    var row = table.insertRow(table.rows.length - 1);
+    row.setAttribute('id', 'row'+rownum);
     
     var invnum_cell = document.createElement('TD');
 
     
     var invnum_cell = document.createElement('TD');
 
@@ -676,7 +682,7 @@ Example:
 %       } else {
 %         $value = $param->{"$field$row"}; 
 %       }
 %       } else {
 %         $value = $param->{"$field$row"}; 
 %       }
-        var my_text = document.createTextNode('<% $value %>');
+        var my_text = document.createTextNode(<% $value |js_string %>);
         my_cell.appendChild(my_text);
 %     }
 
         my_cell.appendChild(my_text);
 %     }
 
@@ -686,10 +692,17 @@ Example:
       my_input.setAttribute('id',   '<% $name %>'+<% $opt{prefix} %>rownum);
       my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
       my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
       my_input.setAttribute('id',   '<% $name %>'+<% $opt{prefix} %>rownum);
       my_input.style.textAlign = '<% $align{ $opt{align}->[$col] || 'l' } %>';
       my_input.setAttribute('size', <% $sizes->[$col] || 10 %>);
-%     if ($types->[$col] eq 'immutable') {
+      my_input.setAttribute('rownum', <% $opt{prefix} %>rownum);
+%     if ( $types->[$col] eq 'immutable' ) {
         my_input.setAttribute('type', 'hidden');
 %     }
         my_input.setAttribute('type', 'hidden');
 %     }
-%     if ( $opt{footer}->[$col] eq '_TOTAL' ) {
+%     elsif ( $types->[$col] eq 'checkbox' ) {
+        my_input.setAttribute('type', 'checkbox');
+%     }
+%     if ( $opt{onchange}->[$col] ) {
+        my_input.onchange   = <% $opt{onchange}->[$col] %>;
+%     }
+%     elsif ( $opt{footer}->[$col] eq '_TOTAL' ) {
         my_input.onchange   = <% $opt{prefix} %>calc_total<%$col%>;
         my_input.onkeyup    = <% $opt{prefix} %>calc_total<%$col%>;
 %     }
         my_input.onchange   = <% $opt{prefix} %>calc_total<%$col%>;
         my_input.onkeyup    = <% $opt{prefix} %>calc_total<%$col%>;
 %     }
@@ -713,6 +726,11 @@ Example:
           + ' <% PL($opt{name_singular} || 'customer') %>';
     }
 
           + ' <% PL($opt{name_singular} || 'customer') %>';
     }
 
+% if ( $opt{add_row_callback} ) {
+    <% $opt{add_row_callback} %>(<% $opt{prefix} %>rownum,
+                                 '<% $opt{prefix} %>');
+% }
+
     <% $opt{prefix} %>rownum++;
 
   }
     <% $opt{prefix} %>rownum++;
 
   }
index 2e79865..45459f1 100644 (file)
@@ -1,6 +1,9 @@
-<% include('/elements/header.html', 'Quick payment entry') %>
+<& /elements/header.html, {
+  title => 'Quick payment entry',
+  etc   => 'onload="preload()"'
+} &>
 
 
-<% include('/elements/error.html') %>
+<& /elements/error.html &>
 
 <SCRIPT TYPE="text/javascript">
 function warnUnload() {
 
 <SCRIPT TYPE="text/javascript">
 function warnUnload() {
@@ -14,6 +17,18 @@ function warnUnload() {
 }
 window.onbeforeunload = warnUnload;
 
 }
 window.onbeforeunload = warnUnload;
 
+function add_row_callback(rownum, prefix) {
+  document.getElementById('enable_app'+rownum).disabled = true;
+}
+
+function custnum_update_callback(rownum, prefix) {
+  var custnum = document.getElementById('custnum'+rownum).value;
+  document.getElementById('enable_app'+rownum).disabled = (custnum == 0);
+% if ( $use_discounts ) {
+  select_discount_term(rownum, prefix);
+% }
+}
+
 function select_discount_term(row, prefix) {
   var custnum_obj = document.getElementById('custnum'+prefix+row);
   var select_obj = document.getElementById('discount_term'+prefix+row);
 function select_discount_term(row, prefix) {
   var custnum_obj = document.getElementById('custnum'+prefix+row);
   var select_obj = document.getElementById('discount_term'+prefix+row);
@@ -46,6 +61,265 @@ function select_discount_term(row, prefix) {
   discount_terms(custnum_obj.value, select_discount_term_update);
 
 }
   discount_terms(custnum_obj.value, select_discount_term_update);
 
 }
+
+var invoices_for_row = new Object;
+
+function update_invoices(rownum, invoices) {
+  invoices_for_row[rownum] = new Object;
+  // only called before create_application_row
+  for ( var i=0; i<invoices.length; i++ ) {
+    invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
+  }
+}
+
+function toggle_application_row(ev, next) {
+  if (!next) next = function(){}; //optional continuation
+  var rownum = this.getAttribute('rownum');
+  if ( this.checked ) {
+    var custnum = document.getElementById('custnum'+rownum).value;
+    if (!custnum) return;
+    lock_payment_row(rownum, true);
+    custnum_search_open( custnum, 
+      function(returned) {
+        update_invoices(rownum, JSON.parse(returned));
+        create_application_row(rownum, 0);
+        next.call(this, rownum);
+      }
+    );
+  }
+}
+
+function lock_payment_row(rownum, flag) {
+% foreach (qw(invnum custnum customer)) {
+  obj = document.getElementById('<% $_ %>'+rownum);
+  obj.readOnly = flag;
+% }
+  document.getElementById('enable_app'+rownum).disabled = flag;
+}
+
+function delete_application_row() {
+  var rownum = this.getAttribute('rownum');
+  var appnum = this.getAttribute('appnum');
+  var tr_app = document.getElementById('row'+rownum+'.'+appnum);
+  var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
+  if ( select_invnum.value ) {
+    invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
+  }
+    
+  tr_app.parentNode.removeChild(tr_app);
+  if ( appnum > 0 ) {
+    document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
+  }
+  else {
+    lock_payment_row(rownum, false);
+    document.getElementById('enable_app'+rownum).checked = false;
+  }
+}
+
+function amount_unapplied(rownum) {
+  var appnum = 0;
+  var total = 0;
+  var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
+                       || 0;
+  while (true) {
+    var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
+    if ( input_amount ) {
+      total += parseFloat(input_amount.value || 0);
+      appnum++;
+    }
+    else {
+      return payment_amount - total;
+    }
+  }
+}
+
+var change_app_amount;
+
+function choose_app_invnum() {
+  var rownum = this.getAttribute('rownum');
+  var appnum = this.getAttribute('appnum');
+  var last_invoice = this.curr_invoice;
+  if ( last_invoice ) {
+    invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
+  }
+
+  if ( this.value ) {
+    var this_invoice = invoices_for_row[rownum][this.value];
+    this.curr_invoice = invoices_for_row[rownum][this.value];
+    var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
+    span_owed.innerHTML = this_invoice['owed'] + '&nbsp;';
+    delete invoices_for_row[rownum][this.value];
+
+    var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
+    if ( input_amount.value == '' ) {
+      input_amount.value = 
+        Math.max(
+          0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
+        ).toFixed(2);
+      // trigger onchange
+      change_app_amount.call(input_amount);
+    }
+  }
+}
+
+function focus_app_invnum() {
+% # invoice numbers just display as invoice numbers
+  var rownum = this.getAttribute('rownum');
+  var add_opt = function(obj, value) {
+    var o = document.createElement('OPTION');
+    o.text = value;
+    o.value = value;
+    obj.add(o);
+  }
+  this.options.length = 0;
+  var this_invoice = this.curr_invoice;
+  if ( this_invoice ) {
+    add_opt(this, this_invoice.invnum);
+  } else {
+    add_opt(this, '');
+  }
+  for ( var x in invoices_for_row[rownum] ) {
+    add_opt(this, invoices_for_row[rownum][x].invnum);
+  }
+}
+
+function change_app_amount() {
+  var rownum = this.getAttribute('rownum');
+  var appnum = this.getAttribute('appnum');
+%# maybe some kind of warning if amount_unapplied < 0?
+%# only spawn a new application row if there are open invoices left,
+%# and this is the highest-numbered application row for the customer,
+%# and the sum of the applied amounts is < the amount of the payment,
+  if ( Object.keys(invoices_for_row[rownum]).length > 0
+       && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
+       && amount_unapplied(rownum) > 0 ) {
+
+    create_application_row(rownum, parseInt(appnum) + 1);
+
+  }
+}
+
+function create_application_row(rownum, appnum) {
+  var payment_row = document.getElementById('row'+rownum);
+  var tr_app = document.createElement('TR');
+  tr_app.setAttribute('rownum', rownum);
+  tr_app.setAttribute('appnum', appnum);
+  tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
+  
+  var td_invnum = document.createElement('TD');
+  td_invnum.setAttribute('colspan', 4);
+  td_invnum.style.textAlign = 'right';
+  td_invnum.appendChild(
+    document.createTextNode('<% mt('Apply to Invoice ') %>')
+  );
+  var select_invnum = document.createElement('SELECT');
+  select_invnum.setAttribute('rownum', rownum);
+  select_invnum.setAttribute('appnum', appnum);
+  select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
+  select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
+  select_invnum.style.textAlign = 'right';
+  select_invnum.style.width = '50px';
+  select_invnum.onchange = choose_app_invnum;
+  select_invnum.onfocus  = focus_app_invnum;
+  
+  td_invnum.appendChild(select_invnum);
+  tr_app.appendChild(td_invnum);
+
+  var td_owed = document.createElement('TD');
+  td_owed.style.textAlign= 'right';
+  var span_owed = document.createElement('SPAN');
+  span_owed.setAttribute('rownum', rownum);
+  span_owed.setAttribute('appnum', appnum);
+  span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
+  td_owed.appendChild(span_owed);
+  tr_app.appendChild(td_owed);
+
+  var td_amount = document.createElement('TD');
+  td_amount.style.textAlign = 'right';
+  var input_amount = document.createElement('INPUT');
+  input_amount.size = 6;
+  input_amount.setAttribute('rownum', rownum);
+  input_amount.setAttribute('appnum', appnum);
+  input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
+  input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
+  input_amount.style.textAlign = 'right';
+  input_amount.onchange = change_app_amount;
+  td_amount.appendChild(input_amount);
+  tr_app.appendChild(td_amount);
+
+  var td_delete = document.createElement('TD');
+  td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
+  var button_delete = document.createElement('INPUT');
+  button_delete.setAttribute('rownum', rownum);
+  button_delete.setAttribute('appnum', appnum);
+  button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
+  button_delete.setAttribute('type', 'button');
+  button_delete.setAttribute('value', 'X');
+  button_delete.onclick = delete_application_row;
+  button_delete.style.color = '#ff0000';
+  button_delete.style.fontWeight = 'bold';
+  button_delete.style.paddingLeft = '2px';
+  button_delete.style.paddingRight = '2px';
+  td_delete.appendChild(button_delete);
+  tr_app.appendChild(td_delete);
+
+  var td_error = document.createElement('TD');
+  var span_error = document.createElement('SPAN');
+  span_error.setAttribute('rownum', rownum);
+  span_error.setAttribute('appnum', appnum);
+  span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
+  span_error.style.color = '#ff0000';
+  td_error.appendChild(span_error);
+  tr_app.appendChild(td_error);
+
+  if ( appnum > 0 ) {
+    //remove delete button on the previous row
+    document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
+  }
+  rownum++;
+  var next_row = document.getElementById('row'+rownum); // always exists
+  payment_row.parentNode.insertBefore(tr_app, next_row);
+
+}
+
+%# for error handling--ugly, but the alternative is translating the whole 
+%# process of creating rows into Mason
+var row_array = <% encode_json(\@rows) %>;
+function preload() {
+  var rownum;
+  var appnum;
+  for (rownum=0; rownum < row_array.length; rownum++) {
+    if ( row_array[rownum].length ) {
+      var enable = document.getElementById('enable_app'+rownum);
+      enable.checked = true;
+      var preload_row = function(r) {//continuation from toggle_application_row
+        for (appnum=0; appnum < row_array[r].length; appnum++) {
+          this_app = row_array[r][appnum];
+          var x = r + '.' + appnum;
+          //set invnum
+          var select_invnum = document.getElementById('invnum'+x);
+          focus_app_invnum.call(select_invnum);
+          for (i=0; i<select_invnum.options.length; i++) {
+            if (select_invnum.options[i].value == this_app.invnum) {
+              select_invnum.selectedIndex = i;
+            }
+          }
+          choose_app_invnum.call(select_invnum);
+          //set amount
+          var input_amount = document.getElementById('amount'+x);
+          input_amount.value = this_app.amount;
+
+          //set error
+          var span_error = document.getElementById('error'+x);
+          span_error.innerHTML = this_app.error;
+          change_app_amount.call(input_amount); //creates next row
+        } //for appnum
+      }; //preload_row function
+      toggle_application_row.call(enable, null, preload_row);
+    } // if row_array[rownum].length
+  } //for rownum
+}
+
 </SCRIPT>
 
 <% include('/elements/xmlhttp.html',
 </SCRIPT>
 
 <% include('/elements/xmlhttp.html',
@@ -57,21 +331,26 @@ function select_discount_term(row, prefix) {
 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
 
 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
 
 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
+<& /elements/xmlhttp.html,
+    url => $p.'misc/xmlhttp-cust_bill-search.html',
+    subs => ['custnum_search_open']
+&>
 
 
-<% include( "/elements/customer-table.html",
-              name_singular => 'payment',
-              header  => \@header,
-              fields  => \@fields,
-              type    => \@types,
-              align   => \@align,
-              size    => \@sizes,
-              color   => \@colors,
-              param   => \%param,
-              footer  => \@footer,
-              footer_align => \@footer_align,
-              custnum_update_callback => $custnum_update_callback,
-          )
-%>
+<& /elements/customer-table.html,
+    name_singular => 'payment',
+    header  => \@header,
+    fields  => \@fields,
+    type    => \@types,
+    align   => \@align,
+    size    => \@sizes,
+    color   => \@colors,
+    param   => \%param,
+    footer  => \@footer,
+    footer_align => \@footer_align,
+    onchange => \@onchange,
+    custnum_update_callback => 'custnum_update_callback',
+    add_row_callback => 'add_row_callback',
+&>
 
 <BR>
 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
 
 <BR>
 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
@@ -105,7 +384,8 @@ my @colors  = ( '', '' );
 my %param   = ();
 my @footer  = ( '_TOTAL', '' );
 my @footer_align = ( 'r', 'r' );
 my %param   = ();
 my @footer  = ( '_TOTAL', '' );
 my @footer_align = ( 'r', 'r' );
-my $custnum_update_callback = '';
+my @onchange = ( '', '' );;
+my $use_discounts = '';
 
 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
   #push @header, 'Discount';
 
 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
   #push @header, 'Discount';
@@ -117,9 +397,20 @@ if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
   push @colors, '';
   push @footer, '';
   push @footer_align, '';
   push @colors, '';
   push @footer, '';
   push @footer_align, '';
-  $custnum_update_callback = 'select_discount_term';
+  push @onchange, '';
+  $use_discounts = 'Y';
 }
 
 }
 
+push @header, 'Allocate';
+push @fields, 'enable_app';
+push @types, 'checkbox';
+push @align, 'c';
+push @sizes, '0';
+push @colors, '';
+push @footer, '';
+push @footer_align, '';
+push @onchange, 'toggle_application_row';
+
 #push @header, 'Error';
 push @header, '';
 push @fields, 'error';
 #push @header, 'Error';
 push @header, '';
 push @fields, 'error';
@@ -129,7 +420,34 @@ push @sizes, '0';
 push @colors, '#ff0000';
 push @footer, '';
 push @footer_align, '';
 push @colors, '#ff0000';
 push @footer, '';
 push @footer_align, '';
+push @onchange, '';
 
 $m->comp('/elements/handle_uri_query');
 
 
 $m->comp('/elements/handle_uri_query');
 
+# set up for preloading
+my @rows;
+my @row_errors;
+if ( $cgi->param('error') ) {
+  my $param = $cgi->Vars;
+  my $enum = 0; #errors numbered separately
+  for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+    $rows[$row] = [];
+    $row_errors[$row] = $param->{"error$enum"};
+    $enum++;
+    for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
+      next if !$param->{"invnum$row.$app"};
+      my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') } 
+        qw( invnum amount );
+      $this_app{'error'} = $param->{"error$enum"} || '';
+      $param->{"error$enum"} = ''; # don't pass this error through
+      $rows[$row][$app] = \%this_app;
+      $enum++;
+    }
+  }
+  for( my $row = 0; $row < @row_errors; $row++ ) {
+    $param->{"error$row"} = $row_errors[$row];
+  }
+}
+#warn Dumper {rows => \@rows, row_errors => \@row_errors };
+
 </%init>
 </%init>
index a6b90ea..3b06f3a 100644 (file)
@@ -1,51 +1,69 @@
-%  die "access denied"
-%    unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
-%
-%  my $param = $cgi->Vars;
-%
-%  #my $paybatch = $param->{'paybatch'};
-%  my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
-%
-%  my @cust_pay = ();
-%  #my $row = 0;
-%  #while ( exists($param->{"custnum$row"}) ) {
-%  for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
-%    my $custnum = $param->{"custnum$row"};
-%    my $cust_main;
-%    if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) {
-%      $cust_main = qsearchs({ 
-%        'table'     => 'cust_main',
-%        'hashref'   => { 'custnum' => $1 },
-%        'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
-%      });
-%    }
-%    if ( length($custnum) and !$cust_main ) { # not found, try agent_custid
-%      $cust_main = qsearchs({ 
-%        'table'     => 'cust_main',
-%        'hashref'   => { 'agent_custid' => $custnum },
-%        'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
-%      });
-%    }
-%    $custnum = $cust_main->custnum if $cust_main;
-%    # if !$cust_main, then this will throw an error on batch_insert
-%
-%    push @cust_pay, new FS::cust_pay {
-%                      'custnum'        => $custnum,
-%                      'paid'           => $param->{"paid$row"},
-%                      'payby'          => 'BILL',
-%                      'payinfo'        => $param->{"payinfo$row"},
-%                      'discount_term'  => $param->{"discount_term$row"},
-%                      'paybatch'       => $paybatch,
-%                    }
-%      if    $param->{"custnum$row"}
-%         || $param->{"paid$row"}
-%         || $param->{"payinfo$row"};
-%    #$row++;
-%  }
-%
-%  my @errors = FS::cust_pay->batch_insert(@cust_pay);
-%  my $num_errors = scalar(grep $_, @errors);
-%
+<%init>
+my $DEBUG = 1;
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
+
+my $param = $cgi->Vars;
+warn Dumper($param) if $DEBUG;
+
+#my $paybatch = $param->{'paybatch'};
+my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
+
+my @cust_pay = ();
+#my $row = 0;
+#while ( exists($param->{"custnum$row"}) ) {
+for ( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
+  my $custnum = $param->{"custnum$row"};
+  my $cust_main;
+  if ( $custnum =~ /^(\d+)$/ and $1 <= 2147483647 ) {
+    $cust_main = qsearchs({ 
+      'table'     => 'cust_main',
+      'hashref'   => { 'custnum' => $1 },
+      'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+    });
+  }
+  if ( length($custnum) and !$cust_main ) { # not found, try agent_custid
+    $cust_main = qsearchs({ 
+      'table'     => 'cust_main',
+      'hashref'   => { 'agent_custid' => $custnum },
+      'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+    });
+  }
+  $custnum = $cust_main->custnum if $cust_main;
+  # if !$cust_main, then this will throw an error on batch_insert
+
+  my $cust_pay = new FS::cust_pay {
+                    'custnum'        => $custnum,
+                    'paid'           => $param->{"paid$row"},
+                    'payby'          => 'BILL',
+                    'payinfo'        => $param->{"payinfo$row"},
+                    'discount_term'  => $param->{"discount_term$row"},
+                    'paybatch'       => $paybatch,
+                  }
+    if    $param->{"custnum$row"}
+       || $param->{"paid$row"}
+       || $param->{"payinfo$row"};
+  next if !$cust_pay;
+  #$row++;
+
+  # payment applications, if any
+  my @cust_bill_pay = ();
+  for ( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
+    next if !$param->{"invnum$row.$app"};
+    push @cust_bill_pay, new FS::cust_bill_pay {
+                            'invnum'  => $param->{"invnum$row.$app"},
+                            'amount'  => $param->{"amount$row.$app"}
+                          };
+  }
+  $cust_pay->set('apply_to', \@cust_bill_pay) if scalar(@cust_bill_pay) > 0;
+
+  push @cust_pay, $cust_pay;
+
+}
+
+my @errors = FS::cust_pay->batch_insert(@cust_pay);
+my $num_errors = scalar(grep $_, @errors);
+</%init>
 %  if ( $num_errors ) {
 %
 %    $cgi->param('error', "$num_errors error". ($num_errors>1 ? 's' : '').
 %  if ( $num_errors ) {
 %
 %    $cgi->param('error', "$num_errors error". ($num_errors>1 ? 's' : '').
@@ -65,4 +83,3 @@
 %    
 <% $cgi->redirect(popurl(3). "search/cust_pay.html?magic=paybatch;paybatch=$paybatch") %>
 % } 
 %    
 <% $cgi->redirect(popurl(3). "search/cust_pay.html?magic=paybatch;paybatch=$paybatch") %>
 % } 
-
diff --git a/httemplate/misc/xmlhttp-cust_bill-search.html b/httemplate/misc/xmlhttp-cust_bill-search.html
new file mode 100644 (file)
index 0000000..46f15d1
--- /dev/null
@@ -0,0 +1,18 @@
+<% encode_json(\@return) %>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die 'access denied' unless $curuser->access_right('View invoices');
+my @return;
+if ( $cgi->param('sub') eq 'custnum_search_open' ) { 
+  my $custnum = $cgi->param('arg');
+  #warn "searching invoices for $custnum\n";
+  my $cust_main = FS::cust_main->by_key($custnum);
+  @return = map { 
+    +{ $_->hash, 
+      'owed' => $_->owed }
+  } $cust_main->open_cust_bill
+    if $curuser->agentnums_href->{ $cust_main->agentnum };
+}
+
+</%init>