require 5.006;
use strict;
use Carp;
+use Try::Tiny;
use Scalar::Util qw( blessed );
-use Time::Local qw(timelocal);
-use Data::Dumper;
+use List::Util qw(min);
use Tie::IxHash;
+use File::Temp; #qw( tempfile );
+use Data::Dumper;
+use Time::Local qw(timelocal);
use Date::Format;
#use Date::Manip;
-use File::Temp; #qw( tempfile );
+use Email::Address;
use Business::CreditCard 0.28;
-use List::Util qw(min);
-use Try::Tiny;
use FS::UID qw( dbh driver_name );
use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
use FS::Cursor;
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
if $DEBUG;
+ return "You are not permitted to change customer invoicing terms."
+ if $self->invoice_terms #i.e. not the default
+ && ! $FS::CurrentUser::CurrentUser->access_right('Edit customer invoice terms');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
the customer.
Currently available options are: I<tax_exemption>, I<cust_payby_params>,
-I<contact_params>, I<invoicing_list>.
+I<contact_params>, I<invoicing_list>, and I<move_pkgs>.
The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
of tax names and exemption numbers. FS::cust_main_exemption records will be
I<invoicing_list> is a synonym for the INVOICING_LIST_ARYREF parameter, and
should be used instead if possible.
+If I<move_pkgs> is an arrayref, it will override the list of packages
+to be moved to the new address (see L<FS::cust_location/move_pkgs>.)
+
=cut
sub replace {
&& ! $self->locale
&& $conf->exists('cust_main-require_locale');
+ return "You are not permitted to change customer invoicing terms."
+ if $old->invoice_terms ne $self->invoice_terms
+ && ! $curuser->access_right('Edit customer invoice terms');
+
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $SIG{QUIT} = 'IGNORE';
$implicit_contact->set('emailaddress', $email);
$implicit_contact->set('invoice_dest', 'Y');
$implicit_contact->set('custnum', $self->custnum);
+ my $i_cust_contact =
+ qsearchs('cust_contact', {
+ contactnum => $implicit_contact->contactnum,
+ custnum => $self->custnum,
+ }
+ );
+ if ( $i_cust_contact ) {
+ $implicit_contact->set($_, $i_cust_contact->$_)
+ foreach qw( classnum selfservice_access comment );
+ }
my $error;
if ( $implicit_contact->contactnum ) {
$self->set('ship_location', ''); #flush cache
if ( $old->ship_locationnum and # should only be null during upgrade...
$old->ship_locationnum != $self->ship_locationnum ) {
- $error = $old->ship_location->move_to($self->ship_location);
+ $error = $old->ship_location->move_to($self->ship_location, move_pkgs => $options{'move_pkgs'});
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
my @emails;
foreach my $contact (@contacts) {
foreach my $contact_email ($contact->contact_email) {
- push @emails,
- $contact->firstlast . ' <' . $contact_email->emailaddress . '>';
+ push @emails, Email::Address->new( $contact->firstlast,
+ $contact_email->emailaddress
+ )->format;
}
}
@emails;
my $cust_pkg_ref = '';
my ( $bill_now, $invoice_terms ) = ( 0, '' );
my $locationnum;
+ my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$setup_cost = $_[0]->{setup_cost};
$invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
$locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
$separate_bill = $_[0]->{separate_bill} || '';
+ $discountnum = $_[0]->{setup_discountnum};
+ $discountnum_amount = $_[0]->{setup_discountnum_amount};
+ $discountnum_percent = $_[0]->{setup_discountnum_percent};
} else { # yuck
$amount = shift;
$setup_cost = '';
}
my $cust_pkg = new FS::cust_pkg ( {
- 'custnum' => $self->custnum,
- 'pkgpart' => $pkgpart,
- 'quantity' => $quantity,
- 'start_date' => $start_date,
- 'no_auto' => $no_auto,
- 'separate_bill' => $separate_bill,
- 'locationnum'=> $locationnum,
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $pkgpart,
+ 'quantity' => $quantity,
+ 'start_date' => $start_date,
+ 'no_auto' => $no_auto,
+ 'separate_bill' => $separate_bill,
+ 'locationnum' => $locationnum,
+ 'setup_discountnum' => $discountnum,
+ 'setup_discountnum_amount' => $discountnum_amount,
+ 'setup_discountnum_percent' => $discountnum_percent,
} );
$error = $cust_pkg->insert;
$error ? $error : $cust_pkg;
}
+=item num_cust_attachment_deleted
+
+Returns the number of deleted attachments for this customer (see
+L<FS::num_cust_attachment>).
+
+=cut
+
+sub num_cust_attachments_deleted {
+ my $self = shift;
+ $self->scalar_sql(
+ " SELECT COUNT(*) FROM cust_attachment ".
+ " WHERE custnum = ? AND disabled IS NOT NULL AND disabled > 0",
+ $self->custnum
+ );
+}
+
+=item max_invnum
+
+Returns the most recent invnum (invoice number) for this customer.
+
+=cut
+
+sub max_invnum {
+ my $self = shift;
+ $self->scalar_sql(
+ " SELECT MAX(invnum) FROM cust_bill WHERE custnum = ?",
+ $self->custnum
+ );
+}
+
=item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
Returns all the invoices (see L<FS::cust_bill>) for this customer.
$class->_upgrade_otaker(%opts);
+ # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
+ # existing records will be encrypted in queueable_upgrade (below)
+ unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
+ eval "use FS::Setup";
+ die $@ if $@;
+ FS::Setup::enable_encryption();
+ }
+
+}
+
+sub queueable_upgrade {
+ my $class = shift;
+
+ ### encryption gets turned on in _upgrade_data, above
+
+ eval "use FS::upgrade_journal";
+ die $@ if $@;
+
+ # prior to 2013 (commit f16665c9) payinfo was stored in history if not
+ # encrypted, clear that out before encrypting/tokenizing anything else
+ if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
+ foreach my $table (qw(
+ cust_payby cust_pay_pending cust_pay cust_pay_void cust_refund
+ )) {
+ my $sql =
+ 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+ }
+ FS::upgrade_journal->set_done('clear_payinfo_history');
+ }
+
+ # fix Tokenized paycardtype and encrypt old records
+ if ( ! FS::upgrade_journal->is_done('paycardtype_Tokenized')
+ || ! FS::upgrade_journal->is_done('encryption_check')
+ )
+ {
+
+ # allow replacement of closed cust_pay/cust_refund records
+ local $FS::payinfo_Mixin::allow_closed_replace = 1;
+
+ # because it looks like nothing's changing
+ local $FS::Record::no_update_diff = 1;
+
+ # commit everything immediately
+ local $FS::UID::AutoCommit = 1;
+
+ # encrypt what's there
+ foreach my $table (qw(
+ cust_payby cust_pay_pending cust_pay cust_pay_void cust_refund
+ )) {
+ my $tclass = 'FS::'.$table;
+ my $lastrecnum = 0;
+ my @recnums = ();
+ while (
+ my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)
+ ) {
+ my $record = $tclass->by_key($recnum);
+ next unless $record; # small chance it's been deleted, that's ok
+ next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
+ # window for possible conflict is practically nonexistant,
+ # but just in case...
+ $record = $record->select_for_update;
+ if (!$record->custnum && $table eq 'cust_pay_pending') {
+ $record->set('custnum_pending',1);
+ }
+ $record->paycardtype('') if $record->paycardtype eq 'Tokenized';
+
+ local($ignore_expired_card) = 1;
+ local($ignore_banned_card) = 1;
+ local($skip_fuzzyfiles) = 1;
+ local($import) = 1;#prevent automatic geocoding (need its own variable?)
+
+ my $error = $record->replace;
+ die "Error replacing $table ".$record->get($record->primary_key).": $error" if $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done('paycardtype_Tokenized');
+ FS::upgrade_journal->set_done('encryption_check') if $conf->exists('encryption');
+ }
+
+ # now that everything's encrypted, tokenize...
+ FS::cust_main::Billing_Realtime::token_check(@_);
+}
+
+# not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
+# cust_payby might get deleted while this runs
+# not a method!
+sub _upgrade_next_recnum {
+ my ($dbh,$table,$lastrecnum,$recnums) = @_;
+ my $recnum = shift @$recnums;
+ return $recnum if $recnum;
+ my $tclass = 'FS::'.$table;
+ my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : '';
+ my $sql = 'SELECT '.$tclass->primary_key.
+ ' FROM '.$table.
+ ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
+ " AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
+ " AND ( length(payinfo) < 80$paycardtypecheck ) ".
+ ' ORDER BY '.$tclass->primary_key.' LIMIT 500';
+ my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+ my @recnums;
+ while (my $rec = $sth->fetchrow_hashref) {
+ push @$recnums, $rec->{$tclass->primary_key};
+ }
+ $sth->finish();
+ $$lastrecnum = $$recnums[-1];
+ return shift @$recnums;
}
=back