4 use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
6 use vars qw( $DEBUG $me $conf @encrypted_fields
7 $unsuspendauto $ignore_noapply
10 use Business::CreditCard;
12 use FS::UID qw( getotaker );
13 use FS::Misc qw( send_email );
14 use FS::Record qw( dbh qsearch qsearchs );
17 use FS::cust_main_Mixin;
18 use FS::payinfo_transaction_Mixin;
20 use FS::cust_bill_pay;
21 use FS::cust_pay_refund;
24 use FS::cust_pay_void;
28 $me = '[FS::cust_pay]';
32 #ask FS::UID to run this stuff for us later
33 FS::UID->install_callback( sub {
35 $unsuspendauto = $conf->exists('unsuspendauto');
38 @encrypted_fields = ('payinfo');
42 FS::cust_pay - Object methods for cust_pay objects
48 $record = new FS::cust_pay \%hash;
49 $record = new FS::cust_pay { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
61 An FS::cust_pay object represents a payment; the transfer of money from a
62 customer. FS::cust_pay inherits from FS::Record. The following fields are
69 primary key (assigned automatically for new payments)
73 customer (see L<FS::cust_main>)
77 specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
78 L<Time::Local> and L<Date::Parse> for conversion functions.
82 Amount of this payment
86 order taker (see L<FS::access_user>)
90 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
94 Payment Information (See L<FS::payinfo_Mixin> for data format)
98 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
102 text field for tracking card processing or other batch grouping
106 Optional unique identifer to prevent duplicate transactions.
110 books closed flag, empty or `Y'
114 Desired pkgnum when using experimental package balances.
118 The bank where the payment was deposited.
122 The name of the depositor.
126 The deposit account number.
140 Creates a new payment. To add the payment to the databse, see L<"insert">.
144 sub table { 'cust_pay'; }
145 sub cust_linked { $_[0]->cust_main_custnum; }
146 sub cust_unlinked_msg {
148 "WARNING: can't find cust_main.custnum ". $self->custnum.
149 ' (cust_pay.paynum '. $self->paynum. ')';
152 =item insert [ OPTION => VALUE ... ]
154 Adds this payment to the database.
156 For backwards-compatibility and convenience, if the additional field invnum
157 is defined, an FS::cust_bill_pay record for the full amount of the payment
158 will be created. In this case, custnum is optional.
160 If the additional field discount_term is defined then a prepayment discount
161 is taken for that length of time. It is an error for the customer to owe
162 after this payment is made.
164 A hash of optional arguments may be passed. Currently "manual" is supported.
165 If true, a payment receipt is sent instead of a statement when
166 'payment_receipt_email' configuration option is set.
171 my($self, %options) = @_;
173 local $SIG{HUP} = 'IGNORE';
174 local $SIG{INT} = 'IGNORE';
175 local $SIG{QUIT} = 'IGNORE';
176 local $SIG{TERM} = 'IGNORE';
177 local $SIG{TSTP} = 'IGNORE';
178 local $SIG{PIPE} = 'IGNORE';
180 my $oldAutoCommit = $FS::UID::AutoCommit;
181 local $FS::UID::AutoCommit = 0;
185 if ( $self->invnum ) {
186 $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
188 $dbh->rollback if $oldAutoCommit;
189 return "Unknown cust_bill.invnum: ". $self->invnum;
191 $self->custnum($cust_bill->custnum );
194 my $error = $self->check;
195 return $error if $error;
197 my $cust_main = $self->cust_main;
198 my $old_balance = $cust_main->balance;
200 $error = $self->SUPER::insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "error inserting cust_pay: $error";
206 if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
207 if ( my $months = $self->discount_term ) {
208 # XXX this should be moved out somewhere, but discount_term_values
210 my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
211 return "can't accept prepayment for an unbilled customer" if !$cust_bill;
213 # %billing_pkgs contains this customer's active monthly packages.
214 # Recurring fees for those packages will be credited and then rebilled
215 # for the full discount term. Other packages on the last invoice
216 # (canceled, non-monthly recurring, or one-time charges) will be
218 my %billing_pkgs = map { $_->pkgnum => $_ }
219 grep { $_->part_pkg->freq eq '1' }
220 $cust_main->billing_pkgs;
221 my $credit = 0; # sum of recurring charges from that invoice
222 my $last_bill_date = 0; # the real bill date
223 foreach my $item ( $cust_bill->cust_bill_pkg ) {
224 next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
225 $credit += $item->recur;
226 $last_bill_date = $item->cust_pkg->last_bill
227 if defined($item->cust_pkg)
228 and $item->cust_pkg->last_bill > $last_bill_date
231 my $cust_credit = new FS::cust_credit {
232 'custnum' => $self->custnum,
233 'amount' => sprintf('%.2f', $credit),
234 'reason' => 'customer chose to prepay for discount',
236 $error = $cust_credit->insert('reason_type' => $credit_type);
238 $dbh->rollback if $oldAutoCommit;
239 return "error inserting prepayment credit: $error";
243 # bill for the entire term
244 $_->bill($_->last_bill) foreach (values %billing_pkgs);
245 $error = $cust_main->bill(
246 # no recurring_only, we want unbilled packages with start dates to
248 'no_usage_reset' => 1,
249 'time' => $last_bill_date, # not $cust_bill->_date
250 'pkg_list' => [ values %billing_pkgs ],
251 'freq_override' => $months,
254 $dbh->rollback if $oldAutoCommit;
255 return "error inserting cust_pay: $error";
257 $error = $cust_main->apply_payments_and_credits;
259 $dbh->rollback if $oldAutoCommit;
260 return "error inserting cust_pay: $error";
262 my $new_balance = $cust_main->balance;
263 if ($new_balance > 0) {
264 $dbh->rollback if $oldAutoCommit;
265 return "balance after prepay discount attempt: $new_balance";
267 # user friendly: override the "apply only to this invoice" mode
274 if ( $self->invnum ) {
275 my $cust_bill_pay = new FS::cust_bill_pay {
276 'invnum' => $self->invnum,
277 'paynum' => $self->paynum,
278 'amount' => $self->paid,
279 '_date' => $self->_date,
281 $error = $cust_bill_pay->insert(%options);
283 if ( $ignore_noapply ) {
284 warn "warning: error inserting cust_bill_pay: $error ".
285 "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
287 $dbh->rollback if $oldAutoCommit;
288 return "error inserting cust_bill_pay: $error";
293 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
295 #false laziness w/ cust_credit::insert
296 if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
297 my @errors = $cust_main->unsuspend;
299 # side-fx with nested transactions? upstack rolls back?
300 warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
306 #bill setup fees for voip_cdr bill_every_call packages
307 #some false laziness w/search in freeside-cdrd
309 'LEFT JOIN part_pkg USING ( pkgpart ) '.
310 "LEFT JOIN part_pkg_option
311 ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
312 AND part_pkg_option.optionname = 'bill_every_call' )";
314 my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
315 " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
317 my @cust_pkg = qsearch({
318 'table' => 'cust_pkg',
319 'addl_from' => $addl_from,
320 'hashref' => { 'custnum' => $self->custnum,
324 'extra_sql' => $extra_sql,
328 warn "voip_cdr bill_every_call packages found; billing customer\n";
329 my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
331 warn "WARNING: Error billing customer: $bill_error\n";
334 #end of billing setup fees for voip_cdr bill_every_call packages
336 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
339 my $trigger = $conf->config('payment_receipt-trigger',
340 $self->cust_main->agentnum) || 'cust_pay';
341 if ( $trigger eq 'cust_pay' ) {
342 my $error = $self->send_receipt(
343 'manual' => $options{'manual'},
344 'cust_bill' => $cust_bill,
345 'cust_main' => $cust_main,
347 warn "can't send payment receipt/statement: $error" if $error;
354 =item void [ REASON ]
356 Voids this payment: deletes the payment and all associated applications and
357 adds a record of the voided payment to the FS::cust_pay_void table.
364 local $SIG{HUP} = 'IGNORE';
365 local $SIG{INT} = 'IGNORE';
366 local $SIG{QUIT} = 'IGNORE';
367 local $SIG{TERM} = 'IGNORE';
368 local $SIG{TSTP} = 'IGNORE';
369 local $SIG{PIPE} = 'IGNORE';
371 my $oldAutoCommit = $FS::UID::AutoCommit;
372 local $FS::UID::AutoCommit = 0;
375 my $cust_pay_void = new FS::cust_pay_void ( {
376 map { $_ => $self->get($_) } $self->fields
378 $cust_pay_void->reason(shift) if scalar(@_);
379 my $error = $cust_pay_void->insert;
381 $dbh->rollback if $oldAutoCommit;
385 $error = $self->delete;
387 $dbh->rollback if $oldAutoCommit;
391 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
399 Unless the closed flag is set, deletes this payment and all associated
400 applications (see L<FS::cust_bill_pay> and L<FS::cust_pay_refund>). In most
401 cases, you want to use the void method instead to leave a record of the
406 # very similar to FS::cust_credit::delete
409 return "Can't delete closed payment" if $self->closed =~ /^Y/i;
411 local $SIG{HUP} = 'IGNORE';
412 local $SIG{INT} = 'IGNORE';
413 local $SIG{QUIT} = 'IGNORE';
414 local $SIG{TERM} = 'IGNORE';
415 local $SIG{TSTP} = 'IGNORE';
416 local $SIG{PIPE} = 'IGNORE';
418 my $oldAutoCommit = $FS::UID::AutoCommit;
419 local $FS::UID::AutoCommit = 0;
422 foreach my $app ( $self->cust_bill_pay, $self->cust_pay_refund ) {
423 my $error = $app->delete;
425 $dbh->rollback if $oldAutoCommit;
430 my $error = $self->SUPER::delete(@_);
432 $dbh->rollback if $oldAutoCommit;
436 if ( $conf->exists('deletepayments')
437 && $conf->config('deletepayments') ne '' ) {
439 my $cust_main = $self->cust_main;
441 my $error = send_email(
442 'from' => $conf->config('invoice_from', $self->cust_main->agentnum),
443 #invoice_from??? well as good as any
444 'to' => $conf->config('deletepayments'),
445 'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
447 "This is an automatic message from your Freeside installation\n",
448 "informing you that the following payment has been deleted:\n",
450 'paynum: '. $self->paynum. "\n",
451 'custnum: '. $self->custnum.
452 " (". $cust_main->last. ", ". $cust_main->first. ")\n",
453 'paid: $'. sprintf("%.2f", $self->paid). "\n",
454 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
455 'payby: '. $self->payby. "\n",
456 'payinfo: '. $self->paymask. "\n",
457 'paybatch: '. $self->paybatch. "\n",
462 $dbh->rollback if $oldAutoCommit;
463 return "can't send payment deletion notification: $error";
468 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 =item replace [ OLD_RECORD ]
476 You can, but probably shouldn't modify payments...
478 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
479 supplied, replaces this record. If there is an error, returns the error,
480 otherwise returns false.
486 return "Can't modify closed payment" if $self->closed =~ /^Y/i;
487 $self->SUPER::replace(@_);
492 Checks all fields to make sure this is a valid payment. If there is an error,
493 returns the error, otherwise returns false. Called by the insert method.
500 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
503 $self->ut_numbern('paynum')
504 || $self->ut_numbern('custnum')
505 || $self->ut_numbern('_date')
506 || $self->ut_money('paid')
507 || $self->ut_alphan('otaker')
508 || $self->ut_textn('paybatch')
509 || $self->ut_textn('payunique')
510 || $self->ut_enum('closed', [ '', 'Y' ])
511 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
512 || $self->ut_textn('bank')
513 || $self->ut_alphan('depositor')
514 || $self->ut_numbern('account')
515 || $self->ut_numbern('teller')
516 || $self->payinfo_check()
518 return $error if $error;
520 return "paid must be > 0 " if $self->paid <= 0;
522 return "unknown cust_main.custnum: ". $self->custnum
524 || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
526 $self->_date(time) unless $self->_date;
528 return "invalid discount_term"
529 if ($self->discount_term && $self->discount_term < 2);
531 if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
532 foreach (qw(bank depositor account teller)) {
533 return "$_ required" if $self->get($_) eq '';
537 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
538 # # UNIQUE index should catch this too, without race conditions, but this
539 # # should give a better error message the other 99.9% of the time...
540 # if ( length($self->payunique)
541 # && qsearchs('cust_pay', { 'payunique' => $self->payunique } ) ) {
542 # #well, it *could* be a better error message
543 # return "duplicate transaction".
544 # " - a payment with unique identifer ". $self->payunique.
551 =item send_receipt HASHREF | OPTION => VALUE ...
553 Sends a payment receipt for this payment..
561 Flag indicating the payment is being made manually.
565 Invoice (FS::cust_bill) object. If not specified, the most recent invoice
570 Customer (FS::cust_main) object (for efficiency).
578 my $opt = ref($_[0]) ? shift : { @_ };
580 my $cust_bill = $opt->{'cust_bill'};
581 my $cust_main = $opt->{'cust_main'} || $self->cust_main;
583 my $conf = new FS::Conf;
585 return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
587 my @invoicing_list = $cust_main->invoicing_list_emailonly;
588 return '' unless @invoicing_list;
590 $cust_bill ||= ($cust_main->cust_bill)[-1]; #rather inefficient though?
594 if ( ( exists($opt->{'manual'}) && $opt->{'manual'} )
595 #|| ! $conf->exists('invoice_html_statement')
599 my $msgnum = $conf->config('payment_receipt_msgnum', $cust_main->agentnum);
601 my $msg_template = FS::msg_template->by_key($msgnum);
602 $error = $msg_template->send(
603 'cust_main' => $cust_main,
605 'from_config' => 'payment_receipt_from',
608 } elsif ( $conf->exists('payment_receipt_email') ) {
610 my $receipt_template = new Text::Template (
612 SOURCE => [ map "$_\n", $conf->config('payment_receipt_email') ],
614 warn "can't create payment receipt template: $Text::Template::ERROR";
618 my $payby = $self->payby;
619 my $payinfo = $self->payinfo;
620 $payby =~ s/^BILL$/Check/ if $payinfo;
621 if ( $payby eq 'CARD' || $payby eq 'CHEK' ) {
622 $payinfo = $self->paymask
624 $payinfo = $self->decrypt($payinfo);
626 $payby =~ s/^CHEK$/Electronic check/;
629 'date' => time2str("%a %B %o, %Y", $self->_date),
630 'name' => $cust_main->name,
631 'paynum' => $self->paynum,
632 'paid' => sprintf("%.2f", $self->paid),
633 'payby' => ucfirst(lc($payby)),
634 'payinfo' => $payinfo,
635 'balance' => $cust_main->balance,
636 'company_name' => $conf->config('company_name', $cust_main->agentnum),
639 if ( $opt->{'cust_pkg'} ) {
640 $fill_in{'pkg'} = $opt->{'cust_pkg'}->part_pkg->pkg;
641 #setup date, other things?
645 'from' => $conf->config('invoice_from', $cust_main->agentnum),
646 #invoice_from??? well as good as any
647 'to' => \@invoicing_list,
648 'subject' => 'Payment receipt',
649 'body' => [ $receipt_template->fill_in( HASH => \%fill_in ) ],
654 warn "payment_receipt is on, but no payment_receipt_msgnum\n";
660 my $queue = new FS::queue {
661 'paynum' => $self->paynum,
662 'job' => 'FS::cust_bill::queueable_email',
665 $error = $queue->insert(
666 'invnum' => $cust_bill->invnum,
667 'template' => 'statement',
668 'notice_name' => 'Statement',
674 warn "send_receipt: $error\n" if $error;
679 Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
686 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
687 sort { $a->_date <=> $b->_date
688 || $a->invnum <=> $b->invnum }
689 qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
693 =item cust_pay_refund
695 Returns all applications of refunds (see L<FS::cust_pay_refund>) to this
700 sub cust_pay_refund {
702 map { $_ } #return $self->num_cust_pay_refund unless wantarray;
703 sort { $a->_date <=> $b->_date }
704 qsearch( 'cust_pay_refund', { 'paynum' => $self->paynum } )
711 Returns the amount of this payment that is still unapplied; which is
712 paid minus all payment applications (see L<FS::cust_bill_pay>) and refund
713 applications (see L<FS::cust_pay_refund>).
719 my $amount = $self->paid;
720 $amount -= $_->amount foreach ( $self->cust_bill_pay );
721 $amount -= $_->amount foreach ( $self->cust_pay_refund );
722 sprintf("%.2f", $amount );
727 Returns the amount of this payment that has not been refuned; which is
728 paid minus all refund applications (see L<FS::cust_pay_refund>).
734 my $amount = $self->paid;
735 $amount -= $_->amount foreach ( $self->cust_pay_refund );
736 sprintf("%.2f", $amount );
741 Returns the "paid" field.
756 =item batch_insert CUST_PAY_OBJECT, ...
758 Class method which inserts multiple payments. Takes a list of FS::cust_pay
759 objects. Returns a list, each element representing the status of inserting the
760 corresponding payment - empty. If there is an error inserting any payment, the
761 entire transaction is rolled back, i.e. all payments are inserted or none are.
765 my @errors = FS::cust_pay->batch_insert(@cust_pay);
766 my $num_errors = scalar(grep $_, @errors);
767 if ( $num_errors == 0 ) {
768 #success; all payments were inserted
770 #failure; no payments were inserted.
776 my $self = shift; #class method
778 local $SIG{HUP} = 'IGNORE';
779 local $SIG{INT} = 'IGNORE';
780 local $SIG{QUIT} = 'IGNORE';
781 local $SIG{TERM} = 'IGNORE';
782 local $SIG{TSTP} = 'IGNORE';
783 local $SIG{PIPE} = 'IGNORE';
785 my $oldAutoCommit = $FS::UID::AutoCommit;
786 local $FS::UID::AutoCommit = 0;
792 my $error = $_->insert( 'manual' => 1 );
796 $_->cust_main->apply_payments;
802 $dbh->rollback if $oldAutoCommit;
804 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
813 Returns an SQL fragment to retreive the unapplied amount.
818 my ($class, $start, $end) = @_;
819 my $bill_start = $start ? "AND cust_bill_pay._date <= $start" : '';
820 my $bill_end = $end ? "AND cust_bill_pay._date > $end" : '';
821 my $refund_start = $start ? "AND cust_pay_refund._date <= $start" : '';
822 my $refund_end = $end ? "AND cust_pay_refund._date > $end" : '';
826 ( SELECT SUM(amount) FROM cust_bill_pay
827 WHERE cust_pay.paynum = cust_bill_pay.paynum
828 $bill_start $bill_end )
832 ( SELECT SUM(amount) FROM cust_pay_refund
833 WHERE cust_pay.paynum = cust_pay_refund.paynum
834 $refund_start $refund_end )
843 # Used by FS::Upgrade to migrate to a new database.
847 sub _upgrade_data { #class method
848 my ($class, %opts) = @_;
850 warn "$me upgrading $class\n" if $DEBUG;
853 # otaker/ivan upgrade
856 #not the most efficient, but hey, it only has to run once
858 my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
859 " AND usernum IS NULL ".
860 " AND 0 < ( SELECT COUNT(*) FROM cust_main ".
861 " WHERE cust_main.custnum = cust_pay.custnum ) ";
863 my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
865 my $sth = dbh->prepare($count_sql) or die dbh->errstr;
866 $sth->execute or die $sth->errstr;
867 my $total = $sth->fetchrow_arrayref->[0];
868 #warn "$total cust_pay records to update\n"
870 local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
875 my @cust_pay = qsearch( {
876 'table' => 'cust_pay',
878 'extra_sql' => $where,
879 'order_by' => 'ORDER BY paynum',
882 foreach my $cust_pay (@cust_pay) {
884 my $h_cust_pay = $cust_pay->h_search('insert');
886 next if $cust_pay->otaker eq $h_cust_pay->history_user;
887 #$cust_pay->otaker($h_cust_pay->history_user);
888 $cust_pay->set('otaker', $h_cust_pay->history_user);
890 $cust_pay->set('otaker', 'legacy');
893 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
894 my $error = $cust_pay->replace;
897 warn " *** WARNING: Error updating order taker for payment paynum ".
898 $cust_pay->paynun. ": $error\n";
902 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
905 if ( $DEBUG > 1 && $lastprog + 30 < time ) {
906 warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n";
913 # payinfo N/A upgrade
916 #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
918 my @na_cust_pay = qsearch( {
919 'table' => 'cust_pay',
920 'hashref' => {}, #could be encrypted# { 'payinfo' => 'N/A' },
921 'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
924 foreach my $na ( @na_cust_pay ) {
926 next unless $na->payinfo eq 'N/A';
928 my $cust_pay_pending =
929 qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
930 unless ( $cust_pay_pending ) {
931 warn " *** WARNING: not-yet recoverable N/A card for payment ".
932 $na->paynum. " (no cust_pay_pending)\n";
935 $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
936 my $error = $na->replace;
938 warn " *** WARNING: Error updating payinfo for payment paynum ".
939 $na->paynun. ": $error\n";
946 # otaker->usernum upgrade
949 delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
950 $class->_upgrade_otaker(%opts);
951 $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
961 =item batch_import HASHREF
963 Inserts new payments.
970 my $fh = $param->{filehandle};
971 my $agentnum = $param->{agentnum};
972 my $format = $param->{'format'};
973 my $paybatch = $param->{'paybatch'};
975 # here is the agent virtualization
976 my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
980 if ( $format eq 'simple' ) {
981 @fields = qw( custnum agent_custid paid payinfo );
983 } elsif ( $format eq 'extended' ) {
984 die "unimplemented\n";
988 die "unknown format $format";
991 eval "use Text::CSV_XS;";
994 my $csv = new Text::CSV_XS;
998 local $SIG{HUP} = 'IGNORE';
999 local $SIG{INT} = 'IGNORE';
1000 local $SIG{QUIT} = 'IGNORE';
1001 local $SIG{TERM} = 'IGNORE';
1002 local $SIG{TSTP} = 'IGNORE';
1003 local $SIG{PIPE} = 'IGNORE';
1005 my $oldAutoCommit = $FS::UID::AutoCommit;
1006 local $FS::UID::AutoCommit = 0;
1010 while ( defined($line=<$fh>) ) {
1012 $csv->parse($line) or do {
1013 $dbh->rollback if $oldAutoCommit;
1014 return "can't parse: ". $csv->error_input();
1017 my @columns = $csv->fields();
1021 paybatch => $paybatch,
1025 foreach my $field ( @fields ) {
1027 if ( $field eq 'agent_custid'
1029 && $columns[0] =~ /\S+/ )
1032 my $agent_custid = $columns[0];
1033 my %hash = ( 'agent_custid' => $agent_custid,
1034 'agentnum' => $agentnum,
1037 if ( $cust_pay{'custnum'} !~ /^\s*$/ ) {
1038 $dbh->rollback if $oldAutoCommit;
1039 return "can't specify custnum with agent_custid $agent_custid";
1042 $cust_main = qsearchs({
1043 'table' => 'cust_main',
1044 'hashref' => \%hash,
1045 'extra_sql' => $extra_sql,
1048 unless ( $cust_main ) {
1049 $dbh->rollback if $oldAutoCommit;
1050 return "can't find customer with agent_custid $agent_custid";
1054 $columns[0] = $cust_main->custnum;
1057 $cust_pay{$field} = shift @columns;
1060 my $cust_pay = new FS::cust_pay( \%cust_pay );
1061 my $error = $cust_pay->insert;
1064 $dbh->rollback if $oldAutoCommit;
1065 return "can't insert payment for $line: $error";
1068 if ( $format eq 'simple' ) {
1069 # include agentnum for less surprise?
1070 $cust_main = qsearchs({
1071 'table' => 'cust_main',
1072 'hashref' => { 'custnum' => $cust_pay->custnum },
1073 'extra_sql' => $extra_sql,
1077 unless ( $cust_main ) {
1078 $dbh->rollback if $oldAutoCommit;
1079 return "can't find customer to which payments apply at line: $line";
1082 $error = $cust_main->apply_payments_and_credits;
1084 $dbh->rollback if $oldAutoCommit;
1085 return "can't apply payments to customer for $line: $error";
1093 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1095 return "Empty file!" unless $imported;
1105 Delete and replace methods.
1109 L<FS::cust_pay_pending>, L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>,
1110 schema.html from the base documentation.