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, prefix) {
30 document.getElementById('enable_app'+rownum).disabled = true;
33 function custnum_update_callback(rownum, prefix) {
34 var custnum = document.getElementById('custnum'+rownum).value;
35 // if there is a custnum and more than one open invoice, enable
36 // (and check) the box
37 var show_applications = !(custnum > 0 && num_open_invoices[rownum] > 1);
38 var enable_app_checkbox = document.getElementById('enable_app'+rownum);
39 enable_app_checkbox.disabled = show_applications;
41 % if ( $use_discounts ) {
42 select_discount_term(rownum, prefix);
46 function invnum_update_callback(rownum, prefix) {
47 custnum_update_callback(rownum, prefix);
50 function select_discount_term(row, prefix) {
51 var custnum_obj = document.getElementById('custnum'+prefix+row);
52 var select_obj = document.getElementById('discount_term'+prefix+row);
55 if (select_obj.type == 'hidden') {
56 value = select_obj.value;
59 var term_select = document.createElement('SELECT');
60 term_select.setAttribute('name', 'discount_term'+row);
61 term_select.setAttribute('id', 'discount_term'+row);
62 term_select.setAttribute('rownum', row);
63 term_select.style.display = '';
64 select_obj.parentNode.replaceChild(term_select, select_obj);
65 opt(term_select, '', '1 month');
67 function select_discount_term_update(discount_terms) {
69 var termArray = eval('(' + discount_terms + ')');
70 for ( var t = 0; t < termArray.length; t++ ) {
71 opt(term_select, termArray[t][0], termArray[t][1]);
72 if (termArray[t][0] == value) {
73 term_select.selectedIndex = t+1;
79 discount_terms(custnum_obj.value, select_discount_term_update);
83 var invoices_for_row = new Object;
85 function update_invoices(rownum, invoices) {
86 invoices_for_row[rownum] = new Object;
87 // only called before create_application_row
88 for ( var i=0; i<invoices.length; i++ ) {
89 invoices_for_row[rownum][ invoices[i].invnum ] = invoices[i];
93 function toggle_application_row(ev, next) {
94 if (!next) next = function(){}; //optional continuation
95 var rownum = this.getAttribute('rownum');
97 var custnum = document.getElementById('custnum'+rownum).value;
99 lock_payment_row(rownum, true);
100 custnum_search_open( custnum,
102 update_invoices(rownum, JSON.parse(returned));
103 create_application_row(rownum, 0);
104 next.call(this, rownum);
108 var row = document.getElementById('row'+rownum);
109 var table_rows = row.parentNode.rows;
110 for (i = row.sectionRowIndex; i < table_rows.count; i++) {
111 if ( table_rows[i].id.indexof('row'+rownum+'.') > -1 ) {
112 table_rows.removeChild(table_rows[i]);
117 lock_payment_row(rownum, false);
121 function lock_payment_row(rownum, flag) {
122 % foreach (qw(invnum custnum customer)) {
123 obj = document.getElementById('<% $_ %>'+rownum);
126 document.getElementById('enable_app'+rownum).disabled = flag;
129 function delete_application_row() {
130 var rownum = this.getAttribute('rownum');
131 var appnum = this.getAttribute('appnum');
132 var tr_app = document.getElementById('row'+rownum+'.'+appnum);
133 var select_invnum = document.getElementById('invnum'+rownum+'.'+appnum);
134 if ( select_invnum.value ) {
135 invoices_for_row[rownum][ select_invnum.value ] = select_invnum.curr_invoice;
138 tr_app.parentNode.removeChild(tr_app);
140 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = '';
143 lock_payment_row(rownum, false);
144 document.getElementById('enable_app'+rownum).checked = false;
148 function amount_unapplied(rownum) {
151 var payment_amount = parseFloat(document.getElementById('paid'+rownum).value)
154 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
155 if ( input_amount ) {
156 total += parseFloat(input_amount.value || 0);
160 return payment_amount - total;
165 var change_app_amount;
167 function choose_app_invnum() {
168 var rownum = this.getAttribute('rownum');
169 var appnum = this.getAttribute('appnum');
170 var last_invoice = this.curr_invoice;
171 if ( last_invoice ) {
172 invoices_for_row[rownum][ last_invoice['invnum'] ] = last_invoice;
176 var this_invoice = invoices_for_row[rownum][this.value];
177 this.curr_invoice = invoices_for_row[rownum][this.value];
178 var span_owed = document.getElementById('owed'+rownum+'.'+appnum);
179 span_owed.innerHTML = this_invoice['owed'] + ' ';
180 delete invoices_for_row[rownum][this.value];
182 var input_amount = document.getElementById('amount'+rownum+'.'+appnum);
183 if ( input_amount.value == '' ) {
186 0, Math.min( amount_unapplied(rownum), this_invoice['owed'])
189 change_app_amount.call(input_amount);
194 function focus_app_invnum() {
195 % # invoice numbers just display as invoice numbers
196 var rownum = this.getAttribute('rownum');
197 var add_opt = function(obj, value, label) {
198 var o = document.createElement('OPTION');
203 this.options.length = 0;
204 var this_invoice = this.curr_invoice;
205 if ( this_invoice ) {
206 add_opt(this, this_invoice.invnum, this_invoice.label);
208 add_opt(this, '', '');
210 for ( var x in invoices_for_row[rownum] ) {
212 invoices_for_row[rownum][x].invnum,
213 invoices_for_row[rownum][x].label);
217 function change_app_amount() {
218 var rownum = this.getAttribute('rownum');
219 var appnum = this.getAttribute('appnum');
220 %# maybe some kind of warning if amount_unapplied < 0?
221 %# only spawn a new application row if there are open invoices left,
222 %# and this is the highest-numbered application row for the customer,
223 %# and the sum of the applied amounts is < the amount of the payment,
224 if ( Object.keys(invoices_for_row[rownum]).length > 0
225 && !document.getElementById( 'row'+rownum+'.'+(parseInt(appnum) + 1) )
226 && amount_unapplied(rownum) > 0 ) {
228 create_application_row(rownum, parseInt(appnum) + 1);
232 function create_application_row(rownum, appnum) {
233 var payment_row = document.getElementById('row'+rownum);
234 var tr_app = document.createElement('TR');
235 tr_app.setAttribute('rownum', rownum);
236 tr_app.setAttribute('appnum', appnum);
237 tr_app.setAttribute('id', 'row'+rownum+'.'+appnum);
239 var td_invnum = document.createElement('TD');
240 td_invnum.setAttribute('colspan', 4);
241 td_invnum.style.textAlign = 'right';
242 td_invnum.appendChild(
243 document.createTextNode('<% mt('Apply to Invoice ') %>')
245 var select_invnum = document.createElement('SELECT');
246 select_invnum.setAttribute('rownum', rownum);
247 select_invnum.setAttribute('appnum', appnum);
248 select_invnum.setAttribute('id', 'invnum'+rownum+'.'+appnum);
249 select_invnum.setAttribute('name', 'invnum'+rownum+'.'+appnum);
250 select_invnum.className = 'select_invnum';
251 select_invnum.onchange = choose_app_invnum;
252 select_invnum.onfocus = focus_app_invnum;
254 td_invnum.appendChild(select_invnum);
255 tr_app.appendChild(td_invnum);
257 var td_owed = document.createElement('TD');
258 td_owed.style.textAlign= 'right';
259 var span_owed = document.createElement('SPAN');
260 span_owed.setAttribute('rownum', rownum);
261 span_owed.setAttribute('appnum', appnum);
262 span_owed.setAttribute('id', 'owed'+rownum+'.'+appnum);
263 td_owed.appendChild(span_owed);
264 tr_app.appendChild(td_owed);
266 var td_amount = document.createElement('TD');
267 td_amount.style.textAlign = 'right';
268 var input_amount = document.createElement('INPUT');
269 input_amount.size = 6;
270 input_amount.setAttribute('rownum', rownum);
271 input_amount.setAttribute('appnum', appnum);
272 input_amount.setAttribute('name', 'amount'+rownum+'.'+appnum);
273 input_amount.setAttribute('id', 'amount'+rownum+'.'+appnum);
274 input_amount.style.textAlign = 'right';
275 input_amount.onchange = change_app_amount;
276 td_amount.appendChild(input_amount);
277 tr_app.appendChild(td_amount);
279 var td_delete = document.createElement('TD');
280 td_delete.setAttribute('colspan', <% scalar(@fields)-2 %>);
281 var button_delete = document.createElement('INPUT');
282 button_delete.setAttribute('rownum', rownum);
283 button_delete.setAttribute('appnum', appnum);
284 button_delete.setAttribute('id', 'delete'+rownum+'.'+appnum);
285 button_delete.setAttribute('type', 'button');
286 button_delete.setAttribute('value', 'X');
287 button_delete.onclick = delete_application_row;
288 button_delete.style.color = '#ff0000';
289 button_delete.style.fontWeight = 'bold';
290 button_delete.style.paddingLeft = '2px';
291 button_delete.style.paddingRight = '2px';
292 td_delete.appendChild(button_delete);
293 tr_app.appendChild(td_delete);
295 var td_error = document.createElement('TD');
296 var span_error = document.createElement('SPAN');
297 span_error.setAttribute('rownum', rownum);
298 span_error.setAttribute('appnum', appnum);
299 span_error.setAttribute('id', 'error'+rownum+'.'+appnum);
300 span_error.style.color = '#ff0000';
301 td_error.appendChild(span_error);
302 tr_app.appendChild(td_error);
305 //remove delete button on the previous row
306 document.getElementById('delete'+rownum+'.'+(appnum-1)).style.display = 'none';
309 var next_row = document.getElementById('row'+rownum); // always exists
310 payment_row.parentNode.insertBefore(tr_app, next_row);
314 %# for error handling--ugly, but the alternative is translating the whole
315 %# process of creating rows into Mason
316 var row_array = <% encode_json(\@rows) %>;
320 for (rownum=0; rownum < row_array.length; rownum++) {
321 if ( row_array[rownum].length ) {
322 var enable = document.getElementById('enable_app'+rownum);
323 enable.checked = true;
324 var preload_row = function(r) {//continuation from toggle_application_row
325 for (appnum=0; appnum < row_array[r].length; appnum++) {
326 this_app = row_array[r][appnum];
327 var x = r + '.' + appnum;
329 var select_invnum = document.getElementById('invnum'+x);
330 focus_app_invnum.call(select_invnum);
331 for (i=0; i<select_invnum.options.length; i++) {
332 if (select_invnum.options[i].value == this_app.invnum) {
333 select_invnum.selectedIndex = i;
336 choose_app_invnum.call(select_invnum);
338 var input_amount = document.getElementById('amount'+x);
339 input_amount.value = this_app.amount;
342 var span_error = document.getElementById('error'+x);
343 span_error.innerHTML = this_app.error;
344 change_app_amount.call(input_amount); //creates next row
346 }; //preload_row function
347 toggle_application_row.call(enable, null, preload_row);
348 } // if row_array[rownum].length
354 <% include('/elements/xmlhttp.html',
355 'url' => $p. 'misc/xmlhttp-cust_main-discount_terms.cgi',
356 'subs' => [qw( discount_terms )],
360 <FORM ACTION="process/batch-cust_pay.cgi" NAME="OneTrueForm" METHOD="POST" onsubmit="document.OneTrueForm.btnsubmit.disabled=true;window.onbeforeunload = null;">
362 <!-- <B>Batch</B> <INPUT TYPE="text" NAME="paybatch"><BR><BR> -->
363 <& /elements/xmlhttp.html,
364 url => $p.'misc/xmlhttp-cust_bill-search.html',
365 subs => ['custnum_search_open']
368 <& /elements/customer-table.html,
369 name_singular => 'payment',
378 footer_align => \@footer_align,
379 onchange => \@onchange,
380 custnum_update_callback => 'custnum_update_callback',
381 invnum_update_callback => 'invnum_update_callback',
382 add_row_callback => 'add_row_callback',
386 <INPUT TYPE="button" VALUE="Post payment batch" name="btnsubmit" onclick="window.onbeforeunload = null; document.OneTrueForm.submit(); this.disabled = true;">
390 %if ( $cgi->param('error') ) {
391 <SCRIPT TYPE="text/javascript">
392 % for ( my $row = 0; defined($cgi->param("custnum$row")); $row++ ) {
393 select_discount_term(<% $row %>, '');
398 <% include('/elements/footer.html') %>
403 unless $FS::CurrentUser::CurrentUser->access_right('Post payment batch');
405 my $conf = new FS::Conf;
406 my $money_char = $conf->config('money_char') || '$';
408 my @header = ( 'Amount', 'Check #' );
409 my @fields = ( 'paid', 'payinfo' );
410 my @types = ( '', '' );
411 my @align = ( 'r', 'r' );
412 my @sizes = ( 8, 10 );
413 my @colors = ( '', '' );
415 my @footer = ( '_TOTAL', '' );
416 my @footer_align = ( 'r', 'r' );
417 my @onchange = ( '', '' );;
418 my $use_discounts = '';
420 if ( FS::Record->scalar_sql('SELECT COUNT(*) FROM part_pkg_discount') ) {
421 #push @header, 'Discount';
423 push @fields, 'discount_term';
424 push @types, 'immutable';
429 push @footer_align, '';
431 $use_discounts = 'Y';
434 push @header, 'Allocate';
435 push @fields, 'enable_app';
436 push @types, 'checkbox';
441 push @footer_align, '';
442 push @onchange, 'toggle_application_row';
444 #push @header, 'Error';
446 push @fields, 'error';
447 push @types, 'immutable';
450 push @colors, '#ff0000';
452 push @footer_align, '';
455 $m->comp('/elements/handle_uri_query');
457 # set up for preloading
460 if ( $cgi->param('error') ) {
461 my $param = $cgi->Vars;
462 my $enum = 0; #errors numbered separately
463 for( my $row = 0; exists($param->{"custnum$row"}); $row++ ) {
465 $row_errors[$row] = $param->{"error$enum"};
467 for( my $app = 0; exists($param->{"invnum$row.$app"}); $app++ ) {
468 next if !$param->{"invnum$row.$app"};
469 my %this_app = map { $_ => ($param->{$_.$row.'.'.$app} || '') }
471 $this_app{'error'} = $param->{"error$enum"} || '';
472 $param->{"error$enum"} = ''; # don't pass this error through
473 $rows[$row][$app] = \%this_app;
477 for( my $row = 0; $row < @row_errors; $row++ ) {
478 $param->{"error$row"} = $row_errors[$row];
481 #warn Dumper {rows => \@rows, row_errors => \@row_errors };