1 <& /elements/header.html, {
2 title => 'Quick payment entry',
3 etc => 'onload="preload()"'
6 <& /elements/error.html &>
8 <STYLE TYPE="text/css">
14 font-family: monospace;
17 <SCRIPT TYPE="text/javascript">
18 function warnUnload() {
19 if(document.getElementById("OneTrueTable").rows.length > 3 &&
20 !document.OneTrueForm.btnsubmit.disabled) {
21 return "The current batch will be lost.";
27 window.onbeforeunload = warnUnload;
29 function add_row_callback(rownum, values) {
31 custnum_update_callback(rownum);
33 document.getElementById('enable_app'+rownum).disabled = true;
37 function delete_row_callback(rownum) {
39 var delbutton = document.getElementById('delete'+rownum+'.'+i);
42 delrows[i] = delbutton;
44 delbutton = document.getElementById('delete'+rownum+'.'+i);
46 delrows = delrows.reverse();
47 for (i = 0; i < delrows.length; i++) {
52 function custnum_update_callback(rownum) {
53 var custnum = document.getElementById('custnum'+rownum).value;
54 // if there is a custnum and more than one open invoice, enable
55 // (and check) the box
56 var show_applications = !(custnum > 0 && num_open_invoices[rownum] > 1);
57 var enable_app_checkbox = document.getElementById('enable_app'+rownum);
58 enable_app_checkbox.disabled = show_applications;
60 % if ( $use_discounts ) {
61 select_discount_term(rownum);
65 function invnum_update_callback(rownum) {
66 custnum_update_callback(rownum);
69 function select_discount_term(row) {
70 var custnum_obj = document.getElementById('custnum'+row);
71 var select_obj = document.getElementById('discount_term'+row);
74 if (select_obj.type == 'hidden') {
75 value = select_obj.value;
78 var term_select = document.createElement('SELECT');
79 term_select.setAttribute('name', 'discount_term'+row);
80 term_select.setAttribute('id', 'discount_term'+row);
81 term_select.setAttribute('rownum', row);
82 term_select.style.display = '';
83 select_obj.parentNode.replaceChild(term_select, select_obj);
84 opt(term_select, '', '1 month');
86 function select_discount_term_update(discount_terms) {
88 var termArray = eval('(' + discount_terms + ')');
89 for ( var t = 0; t < termArray.length; t++ ) {
90 opt(term_select, termArray[t][0], termArray[t][1]);
91 if (termArray[t][0] == value) {
92 term_select.selectedIndex = t+1;
98 discount_terms(custnum_obj.value, select_discount_term_update);
102 var invoices_for_row = new Object;
104 var preloading = 0; // the number of preloading threads currently running
106 // callback from toggle_application_row: we've received a list of
107 // the customer's open invoices. store them.
108 function update_invoices(rownum, invoices) {
109 invoices_for_row[rownum] = new Object;
110 // only called before create_application_row
111 for ( var i=0; i<invoices.length; i++ ) {
112 invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
116 function toggle_application_row(ev, next) {
117 if (!next) next = function(){}; //optional continuation
118 var rownum = this.getAttribute('rownum');
119 if ( this.checked ) {
120 // the user has opted to apply the payment to specific invoices.
121 // - lock the customer
122 // - fetch the list of open invoices
123 // - create a row to select an invoice
124 // - then optionally call "next", with this as the invocant
125 // and the rownum as argument; we use this to preload rows.
126 var custnum = document.getElementById('custnum'+rownum).value;
127 if (!custnum) return;
128 lock_payment_row(rownum, true);
129 custnum_search_open( custnum,
131 update_invoices(rownum, JSON.parse(returned));
132 create_application_row(rownum, 0);
133 next.call(this, rownum);
137 // the user has opted not to do that.
138 // - remove all application rows
139 // - unlock the customer
140 var row = document.getElementById('row'+rownum);
141 var table_rows = row.parentNode.rows;
142 for (i = row.sectionRowIndex; i < table_rows.count; i++) {
143 if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
144 table_rows.removeChild(table_rows[i]);
149 lock_payment_row(rownum, false);
153 function lock_payment_row(rownum, flag) {
154 % foreach (qw(invnum custnum customer)) {
155 obj = document.getElementById('<% $_ %>'+rownum);
158 document.getElementById('enable_app'+rownum).disabled = flag;
161 function delete_application_row() {
162 var rownum = this.getAttribute('rownum');
163 var appnum = this.getAttribute('appnum');
164 var tr_app = document.getElementById('row'+rownum+'.'+appnum);
165 var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
166 if ( select_invnum.value ) {
167 invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
170 tr_app.parentNode.removeChild(tr_app);
172 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
175 lock_payment_row(rownum, false);
176 document.getElementById('enable_app'+rownum).checked = false;
180 function amount_unapplied(rownum) {
183 var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
186 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
187 if ( input_amount ) {
188 total += parseFloat(input_amount.value || 0);
192 return payment_amount - total;
197 var change_app_amount;
199 // the user has chosen an invoice. the previously chosen invoice is still
201 // - if there is a value there, put it back on the invoices_for_row list for
203 // - then _remove_ the newly chosen invoice from that list.
204 // - find the "owed" element for this application row and set its value to the
205 // amount owed on that invoice.
206 // - find the "amount" element for this application row and set its value to
207 // either "owed" or the remaining payment amount, whichever is less.
208 // - call change_app_amount() on that element.
209 function choose_app_invnum() {
210 var rownum = this.getAttribute('rownum');
211 var appnum = this.getAttribute('appnum');
212 var last_invoice = this.curr_invoice;
213 if ( last_invoice ) {
214 invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
218 var this_invoice = invoices_for_row[rownum][this.value];
219 this.curr_invoice = invoices_for_row[rownum][this.value];
220 var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
221 span_owed.innerHTML = this_invoice['owed'] + ' ';
222 delete invoices_for_row[rownum][this.value];
224 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
225 if ( input_amount.value == '' ) {
228 0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
231 change_app_amount.call(input_amount);
236 // the invoice selector has gained focus. clear its list of options, and
237 // replace them with the list of open invoices (from invoices_for_row).
238 // if there's already a selected invoice, prepend that to the list.
239 function focus_app_invnum() {
240 var rownum = this.getAttribute('rownum');
241 var add_opt = function(obj, value, label) {
242 var o = document.createElement('OPTION');
247 this.options.length = 0;
248 var this_invoice = this.curr_invoice;
249 if ( this_invoice ) {
250 add_opt(this, this_invoice.invnum, this_invoice.label);
252 add_opt(this, '', '');
254 for ( var x in invoices_for_row[rownum] ) {
256 invoices_for_row[rownum][x].invnum,
257 invoices_for_row[rownum][x].label);
261 // an application amount has been changed. if there's any unapplied payment
262 // amount, and any remaining invoices_for_row, add a blank application row.
263 // (but don't do this while preloading; it will unconditionally add enough
264 // rows to show all the attempted applications)
265 function change_app_amount() {
266 var rownum = this.getAttribute('rownum');
267 var appnum = this.getAttribute('appnum');
269 && Object.keys(invoices_for_row[rownum]).length > 0
270 && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
271 && amount_unapplied(rownum) > 0 ) {
273 create_application_row(rownum, parseInt(appnum) + 1);
277 // we're creating a payment application row.
278 // create the following elements: <TR>, <TD>s, "Apply to invoice" caption,
279 // invnum selector, "owed" display, amount input box, delete button.
280 function create_application_row(rownum, appnum) {
281 var payment_row = document.getElementById('row'+rownum);
282 var tr_app = document.createElement('TR');
283 tr_app.setAttribute('rownum', rownum);
284 tr_app.setAttribute('appnum', appnum);
285 tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
287 var td_invnum = document.createElement('TD');
288 td_invnum.setAttribute('colspan', 4);
289 td_invnum.style.textAlign = 'right';
290 td_invnum.appendChild(
291 document.createTextNode(<% mt('Apply to Invoice ') |js_string %>)
293 var select_invnum = document.createElement('SELECT');
294 select_invnum.setAttribute('rownum', rownum);
295 select_invnum.setAttribute('appnum', appnum);
296 select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
297 select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
298 select_invnum.className = 'select_invnum';
299 select_invnum.onchange = choose_app_invnum;
300 select_invnum.onfocus = focus_app_invnum;
302 td_invnum.appendChild(select_invnum);
303 tr_app.appendChild(td_invnum);
305 var td_owed = document.createElement('TD');
306 td_owed.style.textAlign= 'right';
307 var span_owed = document.createElement('SPAN');
308 span_owed.setAttribute('rownum', rownum);
309 span_owed.setAttribute('appnum', appnum);
310 span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
311 td_owed.appendChild(span_owed);
312 tr_app.appendChild(td_owed);
314 var td_amount = document.createElement('TD');
315 td_amount.style.textAlign = 'right';
316 var input_amount = document.createElement('INPUT');
317 input_amount.size = 6;
318 input_amount.setAttribute('rownum', rownum);
319 input_amount.setAttribute('appnum', appnum);
320 input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
321 input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
322 input_amount.style.textAlign = 'right';
323 input_amount.onchange = change_app_amount;
324 td_amount.appendChild(input_amount);
325 tr_app.appendChild(td_amount);
327 var td_delete = document.createElement('TD');
328 td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
329 var button_delete = document.createElement('INPUT');
330 button_delete.setAttribute('rownum', rownum);
331 button_delete.setAttribute('appnum', appnum);
332 button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
333 button_delete.setAttribute('type', 'button');
334 button_delete.setAttribute('value', 'X');
335 button_delete.onclick = delete_application_row;
336 button_delete.style.color = '#ff0000';
337 button_delete.style.fontWeight = 'bold';
338 button_delete.style.paddingLeft = '2px';
339 button_delete.style.paddingRight = '2px';
340 td_delete.appendChild(button_delete);
341 tr_app.appendChild(td_delete);
343 var td_error = document.createElement('TD');
344 var span_error = document.createElement('SPAN');
345 span_error.setAttribute('rownum', rownum);
346 span_error.setAttribute('appnum', appnum);
347 span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
348 span_error.style.color = '#ff0000';
349 td_error.appendChild(span_error);
350 tr_app.appendChild(td_error);
353 //remove delete button on the previous row
354 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
357 var next_row = document.getElementById('row'+rownum); // always exists
358 payment_row.parentNode.insertBefore(tr_app, next_row);
362 %# for error handling--ugly, but the alternative is translating the whole
363 %# process of creating rows into Mason
364 var row_obj = <% encode_json(\%rows) %>;
368 for (rownum in row_obj) {
369 if ( row_obj[rownum].length ) {
370 var enable = document.getElementById('enable_app'+rownum);
371 enable.checked = true;
372 var preload_row = function(r) {//continuation from toggle_application_row
377 for (appnum=0; appnum < row_obj[r].length; appnum++) {
378 this_app = row_obj[r][appnum];
379 var x = r + '.' + appnum;
381 var select_invnum = document.getElementById('invnum'+x);
382 focus_app_invnum.call(select_invnum);
383 for (i=0; i<select_invnum.options.length; i++) {
384 if (select_invnum.options[i].value == this_app.invnum) {
385 select_invnum.selectedIndex = i;
388 choose_app_invnum.call(select_invnum);
390 var input_amount = document.getElementById('amount'+x);
391 input_amount.value = this_app.amount;
394 var span_error = document.getElementById('error'+x);
395 span_error.innerHTML = this_app.error;
397 // create another row (unconditionally)
398 create_application_row(r, appnum + 1);
406 }; //preload_row function
408 // enable application rows on the selected customer. this creates
409 // the first row, then kicks off preloading.
410 toggle_application_row.call(enable, null, preload_row);
412 } // if (row_obj[rownum].length
418 <% include('/elements/xmlhttp.html',
419 'url' => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
420 'subs' => [qw( discount_terms )],
424 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
426 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
427 <& /elements/xmlhttp.html,
428 url => $p.'misc/xmlhttp-cust_bill-search.html',
429 subs => ['custnum_search_open']
432 <& /elements/customer-table.html,
433 name_singular => 'payment',
442 footer_align => \@footer_align,
443 onchange => \@onchange,
444 custnum_update_callback => 'custnum_update_callback',
445 invnum_update_callback => 'invnum_update_callback',
446 add_row_callback => 'add_row_callback',
447 delete_row_callback => 'delete_row_callback',
451 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" id="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
455 % #XXX I think this can go away completely, but need to test with $use_discount
456 % ###not perl <SCRIPT TYPE="text/javascript">
457 % #foreach my $row ( keys %rows ) {
458 % ###not perl select_discount_term(<% $row %>, '');
460 % ###not perl </SCRIPT>
462 <% include('/elements/footer.html') %>
467 unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
469 my $conf = new FS::Conf;
470 my $money_char = $conf->config('money_char') || '$';
472 my @header = ( 'Amount', 'Check #', 'Date override' );
473 my @fields = ( 'paid', 'payinfo', '_date' );
474 my @types = ( '', '', 'date', );
475 my @align = ( 'r', 'r', 'r' );
476 my @sizes = ( 8, 10, 8 );
477 my @colors = ( '', '', '' );
479 my @footer = ( '_TOTAL', '', '' );
480 my @footer_align = ( 'r', 'r', '' );
481 my @onchange = ( '', '', '' );
482 my $use_discounts = '';
484 # Not entirely sure this works anymore...
485 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
486 #push @header, 'Discount';
488 push @fields, 'discount_term';
489 push @types, 'immutable';
494 push @footer_align, '';
496 $use_discounts = 'Y';
499 push @header, 'Allocate';
500 push @fields, 'enable_app';
501 push @types, 'checkbox';
506 push @footer_align, '';
507 push @onchange, 'toggle_application_row';
509 push @header, 'No Auto Allocate';
510 push @fields, 'no_auto_apply';
511 push @types, 'checkbox';
516 push @footer_align, '';
519 #push @header, 'Error';
521 push @fields, 'error';
522 push @types, 'immutable';
525 push @colors, '#ff0000';
527 push @footer_align, '';
530 $m->comp('/elements/handle_uri_query');
532 # set up for preloading
535 if ( $cgi->param('error') ) {
536 my $param = $cgi->Vars;
537 my $enum = 0; #errors numbered separately
538 my @invrows = grep /^invnum\d+\.\d+$/, keys %$param; #pare down possibilities
539 foreach my $row ( sort { $a <=> $b } map /^custnum(\d+)$/, keys %$param ) {
540 # for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
542 $row_errors{$row} = $param->{"error$enum"};
544 foreach my $app ( map /^invnum$row\.(\d+)$/, @invrows ) {
545 next if !$param->{"invnum$row.$app"};
546 my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') }
548 $this_app{'error'} = $param->{"error$enum"} || '';
549 $param->{"error$enum"} = ''; # don't pass this error through
550 $rows{$row}[$app] = \%this_app;
554 foreach my $row (keys %rows) {
555 $param->{"error$row"} = $row_errors{$row};
558 #warn Dumper {rows => \%rows, row_errors => \%row_errors };