#use Date::Manip;
use File::Temp; #qw( tempfile );
use Business::CreditCard 0.28;
-use Locale::Country;
use FS::UID qw( dbh driver_name );
use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
use FS::Cursor;
-use FS::Misc qw( generate_email send_email generate_ps do_print money_pretty );
+use FS::Misc qw( generate_ps do_print money_pretty );
use FS::Msgcat qw(gettext);
use FS::CurrentUser;
use FS::TicketSystem;
);
$cust_main->insert( \%hash );
-INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
-be set as the invoicing list (see L<"invoicing_list">). Errors return as
-expected and rollback the entire transaction; it is not necessary to call
-check_invoicing_list first. The invoicing_list is set after the records in the
-CUST_PKG_HASHREF above are inserted, so it is now possible to set an
-invoicing_list destination to the newly-created svc_acct. Here's an example:
-
- $cust_main->insert( {}, [ $email, 'POST' ] );
+INVOICING_LIST_ARYREF: No longer supported.
Currently available options are: I<depend_jobnum>, I<noexport>,
I<tax_exemption>, I<prospectnum>, I<contact> and I<contact_params>.
If I<prospectnum> is set, moves contacts and locations from that prospect.
-If I<contact> is set to an arrayref of FS::contact objects, inserts those
-new contacts with this new customer.
+If I<contact> is set to an arrayref of FS::contact objects, those will be
+inserted.
If I<contact_params> is set to a hashref of CGI parameters (and I<contact> is
unset), inserts those new contacts with this new customer. Handles CGI
sub insert {
my $self = shift;
my $cust_pkgs = @_ ? shift : {};
- my $invoicing_list = @_ ? shift : '';
+ my $invoicing_list;
+ if ( $_[0] and ref($_[0]) eq 'ARRAY' ) {
+ warn "cust_main::insert using deprecated invoicing list argument";
+ $invoicing_list = shift;
+ }
my %options = @_;
warn "$me insert called with options ".
join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
}
}
- warn " setting invoicing list\n"
- if $DEBUG > 1;
-
- if ( $invoicing_list ) {
- $error = $self->check_invoicing_list( $invoicing_list );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- #return "checking invoicing_list (transaction rolled back): $error";
- return $error;
- }
- $self->invoicing_list( $invoicing_list );
- }
-
warn " setting customer tags\n"
if $DEBUG > 1;
warn " setting contacts\n"
if $DEBUG > 1;
+ $invoicing_list ||= $options{'invoicing_list'};
+ if ( $invoicing_list ) {
+
+ $invoicing_list = [ $invoicing_list ] if !ref($invoicing_list);
+
+ my $email = '';
+ foreach my $dest (@$invoicing_list ) {
+ if ($dest eq 'POST') {
+ $self->set('postal_invoice', 'Y');
+ } else {
+
+ my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
+ if ( $contact_email ) {
+ my $cust_contact = FS::cust_contact->new({
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ });
+ $cust_contact->set('invoice_dest', 'Y');
+ my $error = $cust_contact->contactnum ?
+ $cust_contact->replace : $cust_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (linking to email address $dest)";
+ }
+
+ } else {
+ # this email address is not yet linked to any contact
+ $email .= ',' if length($email);
+ $email .= $dest;
+ }
+ }
+ }
+
+ my $contact = FS::contact->new({
+ 'custnum' => $self->get('custnum'),
+ 'last' => $self->get('last'),
+ 'first' => $self->get('first'),
+ 'emailaddress' => $email,
+ 'invoice_dest' => 'Y', # yes, you can set this via the contact
+ });
+ my $error = $contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
if ( my $contact = delete $options{'contact'} ) {
foreach my $c ( @$contact ) {
warn " setting cust_payby\n"
if $DEBUG > 1;
- if ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
+ if ( $options{cust_payby} ) {
+
+ foreach my $cust_payby ( @{ $options{cust_payby} } ) {
+ $cust_payby->custnum($self->custnum);
+ my $error = $cust_payby->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ } elsif ( my $cust_payby_params = delete $options{'cust_payby_params'} ) {
my $error = $self->process_o2m(
'table' => 'cust_payby',
C<ship_location>. The address will still only change if at least one of the
address fields differs from the existing values.
-INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
-be set as the invoicing list (see L<"invoicing_list">). Errors return as
-expected and rollback the entire transaction; it is not necessary to call
-check_invoicing_list first. Here's an example:
-
- $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
+INVOICING_LIST_ARYREF: If you pass an arrayref to this method, it will be
+set as the contact email address for a default contact with the same name as
+the customer.
-Currently available options are: I<tax_exemption>.
+Currently available options are: I<tax_exemption>, I<cust_payby_params>,
+I<contact_params>, I<invoicing_list>.
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
deleted and inserted as appropriate.
+I<cust_payby_params> and I<contact_params> can be hashrefs of named parameter
+groups (describing the customer's payment methods and contacts, respectively)
+in the style supported by L<FS::o2m_Common/process_o2m>. See L<FS::cust_payby>
+and L<FS::contact> for the fields these can contain.
+
+I<invoicing_list> is a synonym for the INVOICING_LIST_ARYREF parameter, and
+should be used instead if possible.
+
=cut
sub replace {
$self->set($l.'num', $new_loc->locationnum);
} #for $l
+ my $invoicing_list;
+ if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
+ warn "cust_main::replace: using deprecated invoicing list argument";
+ $invoicing_list = shift @param;
+ }
+
+ my %options = @param;
+
+ $invoicing_list ||= $options{invoicing_list};
+
+ my @contacts = map { $_->contact } $self->cust_contact;
+ # find a contact that matches the customer's name
+ my ($implicit_contact) = grep { $_->first eq $old->get('first')
+ and $_->last eq $old->get('last') }
+ @contacts;
+ $implicit_contact ||= FS::contact->new({
+ 'custnum' => $self->custnum,
+ 'locationnum' => $self->get('bill_locationnum'),
+ });
+
+ # for any of these that are already contact emails, link to the existing
+ # contact
+ if ( $invoicing_list ) {
+ my $email = '';
+
+ # kind of like process_m2m on these, except:
+ # - the other side is two tables in a join
+ # - and we might have to create new contact_emails
+ # - and possibly a new contact
+ #
+ # Find existing invoice emails that aren't on the implicit contact.
+ # Any of these that are not on the new invoicing list will be removed.
+ my %old_email_cust_contact;
+ foreach my $cust_contact ($self->cust_contact) {
+ next if !$cust_contact->invoice_dest;
+ next if $cust_contact->contactnum == ($implicit_contact->contactnum || 0);
+
+ foreach my $contact_email ($cust_contact->contact->contact_email) {
+ $old_email_cust_contact{ $contact_email->emailaddress } = $cust_contact;
+ }
+ }
+
+ foreach my $dest (@$invoicing_list) {
+
+ if ($dest eq 'POST') {
+
+ $self->set('postal_invoice', 'Y');
+
+ } elsif ( exists($old_email_cust_contact{$dest}) ) {
+
+ delete $old_email_cust_contact{$dest}; # don't need to remove it, then
+
+ } else {
+
+ # See if it belongs to some other contact; if so, link it.
+ my $contact_email = qsearchs('contact_email', { emailaddress => $dest });
+ if ( $contact_email
+ and $contact_email->contactnum != ($implicit_contact->contactnum || 0) ) {
+ my $cust_contact = qsearchs('cust_contact', {
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ }) || FS::cust_contact->new({
+ contactnum => $contact_email->contactnum,
+ custnum => $self->custnum,
+ });
+ $cust_contact->set('invoice_dest', 'Y');
+ my $error = $cust_contact->custcontactnum ?
+ $cust_contact->replace : $cust_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (linking to email address $dest)";
+ }
+
+ } else {
+ # This email address is not yet linked to any contact, so it will
+ # be added to the implicit contact.
+ $email .= ',' if length($email);
+ $email .= $dest;
+ }
+ }
+ }
+
+ foreach my $remove_dest (keys %old_email_cust_contact) {
+ my $cust_contact = $old_email_cust_contact{$remove_dest};
+ # These were not in the list of requested destinations, so take them off.
+ $cust_contact->set('invoice_dest', '');
+ my $error = $cust_contact->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (unlinking email address $remove_dest)";
+ }
+ }
+
+ # make sure it keeps up with the changed customer name, if any
+ $implicit_contact->set('last', $self->get('last'));
+ $implicit_contact->set('first', $self->get('first'));
+ $implicit_contact->set('emailaddress', $email);
+ $implicit_contact->set('invoice_dest', 'Y');
+ $implicit_contact->set('custnum', $self->custnum);
+
+ my $error;
+ if ( $implicit_contact->contactnum ) {
+ $error = $implicit_contact->replace;
+ } elsif ( length($email) ) { # don't create a new contact if not needed
+ $error = $implicit_contact->insert;
+ }
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$error (adding email address $email)";
+ }
+
+ }
+
# replace the customer record
my $error = $self->SUPER::replace($old);
}
}
- if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
- my $invoicing_list = shift @param;
- $error = $self->check_invoicing_list( $invoicing_list );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- $self->invoicing_list( $invoicing_list );
- }
-
if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
#this could be more efficient than deleting and re-inserting, if it matters
}
- my %options = @param;
-
my $tax_exemption = delete $options{'tax_exemption'};
if ( $tax_exemption ) {
}
+ if ( my $contact_params = delete $options{'contact_params'} ) {
+
+ # this can potentially replace contacts that were created by the
+ # invoicing list argument, but the UI shouldn't allow both of them
+ # to be specified
+
+ my $error = $self->process_o2m(
+ 'table' => 'contact',
+ 'fields' => FS::contact->cgi_contact_fields,
+ 'params' => $contact_params,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
unless ( $import || $skip_fuzzyfiles ) {
$error = $self->queue_fuzzyfiles_update;
if ( $error ) {
|| $self->ut_alphan('po_number')
|| $self->ut_enum('complimentary', [ '', 'Y' ])
|| $self->ut_flag('invoice_ship_address')
+ || $self->ut_flag('invoice_dest')
;
foreach (qw(company ship_company)) {
qsearch('cust_contact', { 'custnum' => $self->custnum } );
}
-=item cust_payby
+=item cust_payby PAYBY
Returns all payment methods (see L<FS::cust_payby>) for this customer.
+If one or more PAYBY are specified, returns only payment methods for specified PAYBY.
+Does not validate PAYBY.
+
=cut
sub cust_payby {
my $self = shift;
- qsearch({
+ my @payby = @_;
+ my $search = {
'table' => 'cust_payby',
'hashref' => { 'custnum' => $self->custnum },
'order_by' => "ORDER BY payby IN ('CARD','CHEK') DESC, weight ASC",
- });
+ };
+ $search->{'extra_sql'} = ' AND payby IN ( ' . join(',', map { dbh->quote($_) } @payby) . ' ) '
+ if @payby;
+
+ qsearch($search);
}
+=item has_cust_payby_auto
+
+Returns true if customer has an automatic payment method ('CARD' or 'CHEK')
+
+=cut
+
sub has_cust_payby_auto {
my $self = shift;
scalar( qsearch({
}
-=item paydate_monthyear
-
-Returns a two-element list consisting of the month and year of this customer's
-paydate (credit card expiration date for CARD customers)
-
-=cut
-
-sub paydate_monthyear {
- my $self = shift;
- if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
- ( $2, $1 );
- } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
- ( $1, $3 );
- } else {
- ('', '');
- }
-}
-
=item paydate_epoch
Returns the exact time in seconds corresponding to the payment method
=item cust_main_exemption
-=item invoicing_list [ ARRAYREF ]
-
-If an arguement is given, sets these email addresses as invoice recipients
-(see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
-(except as warnings), so use check_invoicing_list first.
-
-Returns a list of email addresses (with svcnum entries expanded).
-
-Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
-check it without disturbing anything by passing nothing.
+=item invoicing_list
-This interface may change in the future.
+Returns a list of email addresses (with svcnum entries expanded), and the word
+'POST' if the customer receives postal invoices.
=cut
my( $self, $arrayref ) = @_;
if ( $arrayref ) {
- my @cust_main_invoice;
- if ( $self->custnum ) {
- @cust_main_invoice =
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- @cust_main_invoice = ();
- }
- foreach my $cust_main_invoice ( @cust_main_invoice ) {
- #warn $cust_main_invoice->destnum;
- unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
- #warn $cust_main_invoice->destnum;
- my $error = $cust_main_invoice->delete;
- warn $error if $error;
- }
- }
- if ( $self->custnum ) {
- @cust_main_invoice =
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- @cust_main_invoice = ();
- }
- my %seen = map { $_->address => 1 } @cust_main_invoice;
- foreach my $address ( @{$arrayref} ) {
- next if exists $seen{$address} && $seen{$address};
- $seen{$address} = 1;
- my $cust_main_invoice = new FS::cust_main_invoice ( {
- 'custnum' => $self->custnum,
- 'dest' => $address,
- } );
- my $error = $cust_main_invoice->insert;
- warn $error if $error;
- }
+ warn "FS::cust_main::invoicing_list(ARRAY) is no longer supported.";
}
- if ( $self->custnum ) {
- map { $_->address }
- qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
- } else {
- ();
- }
+ my @emails = $self->invoicing_list_emailonly;
+ push @emails, 'POST' if $self->get('postal_invoice');
+ @emails;
}
=item check_invoicing_list ARRAYREF
'';
}
-=item set_default_invoicing_list
-
-Sets the invoicing list to all accounts associated with this customer,
-overwriting any previous invoicing list.
-
-=cut
-
-sub set_default_invoicing_list {
- my $self = shift;
- $self->invoicing_list($self->all_emails);
-}
-
=item all_emails
Returns the email addresses of all accounts provisioned for this customer.
sub invoicing_list_addpost {
my $self = shift;
- return if grep { $_ eq 'POST' } $self->invoicing_list;
- my @invoicing_list = $self->invoicing_list;
- push @invoicing_list, 'POST';
- $self->invoicing_list(\@invoicing_list);
+ if ( $self->get('postal_invoice') eq '' ) {
+ $self->set('postal_invoice', 'Y');
+ my $error = $self->replace;
+ warn $error if $error; # should fail harder, but this is traditional
+ }
}
=item invoicing_list_emailonly
my $self = shift;
warn "$me invoicing_list_emailonly called"
if $DEBUG;
- grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
+ return () if !$self->custnum; # not yet inserted
+ return map { $_->emailaddress }
+ qsearch({
+ table => 'cust_contact',
+ select => 'emailaddress',
+ addl_from => ' JOIN contact USING (contactnum) '.
+ ' JOIN contact_email USING (contactnum)',
+ hashref => { 'custnum' => $self->custnum, },
+ extra_sql => q( AND cust_contact.invoice_dest = 'Y'),
+ });
}
=item invoicing_list_emailonly_scalar
$contact->get('first') . ' '. $contact->get('last');
}
-#XXX this doesn't work in 3.x+
-#=item country_full
-#
-#Returns this customer's full country name
-#
-#=cut
-#
-#sub country_full {
-# my $self = shift;
-# code2country($self->country);
-#}
-
sub bill_country_full {
my $self = shift;
- code2country($self->bill_location->country);
+ $self->bill_location->country_full;
}
sub ship_country_full {
my $self = shift;
- code2country($self->ship_location->country);
+ $self->ship_location->country_full;
}
=item county_state_county [ PREFIX ]
(@tickets);
}
+=item appointments [ STATUS ]
+
+Returns an array of hashes representing the customer's RT tickets which
+are appointments.
+
+=cut
+
+sub appointments {
+ my $self = shift;
+ my $status = ( @_ && $_[0] ) ? shift : '';
+
+ return () unless $conf->config('ticket_system');
+
+ my $queueid = $conf->config('ticket_system-appointment-queueid');
+
+ @{ FS::TicketSystem->customer_tickets( $self->custnum,
+ 99,
+ undef,
+ $status,
+ $queueid,
+ )
+ };
+}
+
# Return services representing svc_accts in customer support packages
sub support_services {
my $self = shift;
return @out;
}
+=item save_cust_payby
+
+Saves a new cust_payby for this customer, replacing an existing entry only
+in select circumstances. Does not validate input.
+
+If auto is specified, marks this as the customer's primary method (weight 1)
+and changes existing primary methods for that payby to secondary methods (weight 2.)
+If bill_location is specified with auto, also sets location in cust_main.
+
+Will not insert complete duplicates of existing records, or records in which the
+only difference from an existing record is to turn off automatic payment (will
+return without error.) Will replace existing records in which the only difference
+is to add a value to a previously empty preserved field and/or turn on automatic payment.
+Fields marked as preserved are optional, and existing values will not be overwritten with
+blanks when replacing.
+
+Accepts the following named parameters:
+
+payment_payby - either CARD or CHEK
+
+auto - save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
+
+payinfo - required
+
+paymask - optional, but should be specified for anything that might be tokenized, will be preserved when replacing
+
+payname - required
+
+payip - optional, will be preserved when replacing
+
+paydate - CARD only, required
+
+bill_location - CARD only, required, FS::cust_location object
+
+paystart_month - CARD only, optional, will be preserved when replacing
+
+paystart_year - CARD only, optional, will be preserved when replacing
+
+payissue - CARD only, optional, will be preserved when replacing
+
+paycvv - CARD only, only used if conf cvv-save is set appropriately
+
+paytype - CHEK only
+
+paystate - CHEK only
+
+=cut
+
+#The code for this option is in place, but it's not currently used
+#
+# replace - existing cust_payby object to be replaced (must match custnum)
+
+# stateid/stateid_state/ss are not currently supported in cust_payby,
+# might not even work properly in 4.x, but will need to work here if ever added
+
+sub save_cust_payby {
+ my $self = shift;
+ my %opt = @_;
+
+ my $old = $opt{'replace'};
+ my $new = new FS::cust_payby { $old ? $old->hash : () };
+ return "Customer number does not match" if $new->custnum and $new->custnum != $self->custnum;
+ $new->set( 'custnum' => $self->custnum );
+
+ my $payby = $opt{'payment_payby'};
+ return "Bad payby" unless grep(/^$payby$/,('CARD','CHEK'));
+
+ # don't allow turning off auto when replacing
+ $opt{'auto'} ||= 1 if $old and $old->payby !~ /^D/;
+
+ my @check_existing; # payby relevant to this payment_payby
+
+ # set payby based on auto
+ if ( $payby eq 'CARD' ) {
+ $new->set( 'payby' => ( $opt{'auto'} ? 'CARD' : 'DCRD' ) );
+ @check_existing = qw( CARD DCRD );
+ } elsif ( $payby eq 'CHEK' ) {
+ $new->set( 'payby' => ( $opt{'auto'} ? 'CHEK' : 'DCHK' ) );
+ @check_existing = qw( CHEK DCHK );
+ }
+
+ # every automatic payment type added here will be marked primary
+ $new->set( 'weight' => $opt{'auto'} ? 1 : '' );
+
+ # basic fields
+ $new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
+ $new->paymask($opt{'paymask'}) if $opt{'paymask'}; # in case it's been tokenized, override with loaded paymask
+ $new->set( 'payname' => $opt{'payname'} );
+ $new->set( 'payip' => $opt{'payip'} ); # will be preserved below
+
+ my $conf = new FS::Conf;
+
+ # compare to FS::cust_main::realtime_bop - check both to make sure working correctly
+ if ( $payby eq 'CARD' &&
+ grep { $_ eq cardtype($opt{'payinfo'}) } $conf->config('cvv-save') ) {
+ $new->set( 'paycvv' => $opt{'paycvv'} );
+ } else {
+ $new->set( 'paycvv' => '');
+ }
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ # set fields specific to payment_payby
+ if ( $payby eq 'CARD' ) {
+ if ($opt{'bill_location'}) {
+ $opt{'bill_location'}->set('custnum' => $self->custnum);
+ my $error = $opt{'bill_location'}->find_or_insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $new->set( 'locationnum' => $opt{'bill_location'}->locationnum );
+ }
+ foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
+ $new->set( $field => $opt{$field} );
+ }
+ } else {
+ foreach my $field ( qw(paytype paystate) ) {
+ $new->set( $field => $opt{$field} );
+ }
+ }
+
+ # other cust_payby to compare this to
+ my @existing = $self->cust_payby(@check_existing);
+
+ # fields that can overwrite blanks with values, but not values with blanks
+ my @preserve = qw( paymask locationnum paystart_month paystart_year payissue payip );
+
+ my $skip_cust_payby = 0; # true if we don't need to save or reweight cust_payby
+ unless ($old) {
+ # generally, we don't want to overwrite existing cust_payby with this,
+ # but we can replace if we're only marking it auto or adding a preserved field
+ # and we can avoid saving a total duplicate or merely turning off auto
+PAYBYLOOP:
+ foreach my $cust_payby (@existing) {
+ # check fields that absolutely should not change
+ foreach my $field ($new->fields) {
+ next if grep(/^$field$/, qw( custpaybynum payby weight ) );
+ next if grep(/^$field$/, @preserve );
+ next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
+ }
+ # now check fields that can replace if one value is blank
+ my $replace = 0;
+ foreach my $field (@preserve) {
+ if (
+ ( $new->get($field) and !$cust_payby->get($field) ) or
+ ( $cust_payby->get($field) and !$new->get($field) )
+ ) {
+ # prevention of overwriting values with blanks happens farther below
+ $replace = 1;
+ } elsif ( $new->get($field) ne $cust_payby->get($field) ) {
+ next PAYBYLOOP;
+ }
+ }
+ unless ( $replace ) {
+ # nearly identical, now check weight
+ if ($new->get('weight') eq $cust_payby->get('weight') or !$new->get('weight')) {
+ # ignore identical cust_payby, and ignore attempts to turn off auto
+ # no need to save or re-weight cust_payby (but still need to update/commit $self)
+ $skip_cust_payby = 1;
+ last PAYBYLOOP;
+ }
+ # otherwise, only change is to mark this as primary
+ }
+ # if we got this far, we're definitely replacing
+ $old = $cust_payby;
+ last PAYBYLOOP;
+ }
+ }
+
+ if ($old) {
+ $new->set( 'custpaybynum' => $old->custpaybynum );
+ # don't turn off automatic payment (but allow it to be turned on)
+ if ($new->payby =~ /^D/ and $new->payby ne $old->payby) {
+ $opt{'auto'} = 1;
+ $new->set( 'payby' => $old->payby );
+ $new->set( 'weight' => 1 );
+ }
+ # make sure we're not overwriting values with blanks
+ foreach my $field (@preserve) {
+ if ( $old->get($field) and !$new->get($field) ) {
+ $new->set( $field => $old->get($field) );
+ }
+ }
+ }
+
+ # only overwrite cust_main bill_location if auto
+ if ($opt{'auto'} && $opt{'bill_location'}) {
+ $self->set('bill_location' => $opt{'bill_location'});
+ my $error = $self->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ # done with everything except reweighting and saving cust_payby
+ # still need to commit changes to cust_main and cust_location
+ if ($skip_cust_payby) {
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ return '';
+ }
+
+ # re-weight existing primary cust_pay for this payby
+ if ($opt{'auto'}) {
+ foreach my $cust_payby (@existing) {
+ # relies on cust_payby return order
+ last unless $cust_payby->payby !~ /^D/;
+ last if $cust_payby->weight > 1;
+ next if $new->custpaybynum eq $cust_payby->custpaybynum;
+ $cust_payby->set( 'weight' => 2 );
+ my $error = $cust_payby->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error reweighting cust_payby: $error";
+ }
+ }
+ }
+
+ # finally, save cust_payby
+ my $error = $old ? $new->replace($old) : $new->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
=back
=head1 CLASS METHODS
=over 4
-=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
+#=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
-Deprecated. Use event notification and message templates
-(L<FS::msg_template>) instead.
+#Deprecated. Use event notification and message templates
+#(L<FS::msg_template>) instead.
-Sends a templated email notification to the customer (see L<Text::Template>).
+#Sends a templated email notification to the customer (see L<Text::Template>).
-OPTIONS is a hash and may include
+#OPTIONS is a hash and may include
-I<from> - the email sender (default is invoice_from)
+#I<from> - the email sender (default is invoice_from)
-I<to> - comma-separated scalar or arrayref of recipients
- (default is invoicing_list)
+#I<to> - comma-separated scalar or arrayref of recipients
+# (default is invoicing_list)
-I<subject> - The subject line of the sent email notification
- (default is "Notice from company_name")
+#I<subject> - The subject line of the sent email notification
+# (default is "Notice from company_name")
-I<extra_fields> - a hashref of name/value pairs which will be substituted
- into the template
+#I<extra_fields> - a hashref of name/value pairs which will be substituted
+# into the template
-The following variables are vavailable in the template.
+#The following variables are vavailable in the template.
-I<$first> - the customer first name
-I<$last> - the customer last name
-I<$company> - the customer company
-I<$payby> - a description of the method of payment for the customer
- # would be nice to use FS::payby::shortname
-I<$payinfo> - the account information used to collect for this customer
-I<$expdate> - the expiration of the customer payment in seconds from epoch
-
-=cut
-
-sub notify {
- my ($self, $template, %options) = @_;
-
- return unless $conf->exists($template);
-
- my $from = $conf->invoice_from_full($self->agentnum)
- if $conf->exists('invoice_from', $self->agentnum);
- $from = $options{from} if exists($options{from});
-
- my $to = join(',', $self->invoicing_list_emailonly);
- $to = $options{to} if exists($options{to});
-
- my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
- if $conf->exists('company_name', $self->agentnum);
- $subject = $options{subject} if exists($options{subject});
-
- my $notify_template = new Text::Template (TYPE => 'ARRAY',
- SOURCE => [ map "$_\n",
- $conf->config($template)]
- )
- or die "can't create new Text::Template object: Text::Template::ERROR";
- $notify_template->compile()
- or die "can't compile template: Text::Template::ERROR";
-
- $FS::notify_template::_template::company_name =
- $conf->config('company_name', $self->agentnum);
- $FS::notify_template::_template::company_address =
- join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
-
- my $paydate = $self->paydate || '2037-12-31';
- $FS::notify_template::_template::first = $self->first;
- $FS::notify_template::_template::last = $self->last;
- $FS::notify_template::_template::company = $self->company;
- $FS::notify_template::_template::payinfo = $self->mask_payinfo;
- my $payby = $self->payby;
- my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
- my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
-
- #credit cards expire at the end of the month/year of their exp date
- if ($payby eq 'CARD' || $payby eq 'DCRD') {
- $FS::notify_template::_template::payby = 'credit card';
- ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
- $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
- $expire_time--;
- }elsif ($payby eq 'COMP') {
- $FS::notify_template::_template::payby = 'complimentary account';
- }else{
- $FS::notify_template::_template::payby = 'current method';
- }
- $FS::notify_template::_template::expdate = $expire_time;
+#I<$first> - the customer first name
+#I<$last> - the customer last name
+#I<$company> - the customer company
+#I<$payby> - a description of the method of payment for the customer
+# # would be nice to use FS::payby::shortname
+#I<$payinfo> - the account information used to collect for this customer
+#I<$expdate> - the expiration of the customer payment in seconds from epoch
- for (keys %{$options{extra_fields}}){
- no strict "refs";
- ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
- }
+#=cut
- send_email(from => $from,
- to => $to,
- subject => $subject,
- body => $notify_template->fill_in( PACKAGE =>
- 'FS::notify_template::_template' ),
- );
+#sub notify {
+# my ($self, $template, %options) = @_;
+
+# return unless $conf->exists($template);
+
+# my $from = $conf->invoice_from_full($self->agentnum)
+# if $conf->exists('invoice_from', $self->agentnum);
+# $from = $options{from} if exists($options{from});
+
+# my $to = join(',', $self->invoicing_list_emailonly);
+# $to = $options{to} if exists($options{to});
+#
+# my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
+# if $conf->exists('company_name', $self->agentnum);
+# $subject = $options{subject} if exists($options{subject});
+
+# my $notify_template = new Text::Template (TYPE => 'ARRAY',
+# SOURCE => [ map "$_\n",
+# $conf->config($template)]
+# )
+# or die "can't create new Text::Template object: Text::Template::ERROR";
+# $notify_template->compile()
+# or die "can't compile template: Text::Template::ERROR";
+
+# $FS::notify_template::_template::company_name =
+# $conf->config('company_name', $self->agentnum);
+# $FS::notify_template::_template::company_address =
+# join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
+
+# my $paydate = $self->paydate || '2037-12-31';
+# $FS::notify_template::_template::first = $self->first;
+# $FS::notify_template::_template::last = $self->last;
+# $FS::notify_template::_template::company = $self->company;
+# $FS::notify_template::_template::payinfo = $self->mask_payinfo;
+# my $payby = $self->payby;
+# my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
+# my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
+
+# #credit cards expire at the end of the month/year of their exp date
+# if ($payby eq 'CARD' || $payby eq 'DCRD') {
+# $FS::notify_template::_template::payby = 'credit card';
+# ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
+# $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
+# $expire_time--;
+# }elsif ($payby eq 'COMP') {
+# $FS::notify_template::_template::payby = 'complimentary account';
+# }else{
+# $FS::notify_template::_template::payby = 'current method';
+# }
+# $FS::notify_template::_template::expdate = $expire_time;
+
+# for (keys %{$options{extra_fields}}){
+# no strict "refs";
+# ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
+# }
+
+# send_email(from => $from,
+# to => $to,
+# subject => $subject,
+# body => $notify_template->fill_in( PACKAGE =>
+# 'FS::notify_template::_template' ),
+# );
-}
+#}
=item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
into the template. These values may override values mentioned below
and those from the customer record.
+I<template_text> - if present, ignores TEMPLATE_NAME and uses the provided text
+
The following variables are available in the template instead of or in addition
to the fields of the customer record.
sub generate_letter {
my ($self, $template, %options) = @_;
- return unless $conf->exists($template);
+ warn "Template $template does not exist" && return
+ unless $conf->exists($template) || $options{'template_text'};
+
+ my $template_source = $options{'template_text'}
+ ? [ $options{'template_text'} ]
+ : [ map "$_\n", $conf->config($template) ];
my $letter_template = new Text::Template
( TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", $conf->config($template)],
+ SOURCE => $template_source,
DELIMITERS => [ '[@--', '--@]' ],
)
or die "can't create new Text::Template object: Text::Template::ERROR";
my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
or die "invalid customer number: " . $opt{custnum};
- my $error = $self->print( { 'template' => $opt{template} } );
+#do not backport this change to 3.x
+# my $error = $self->print( { 'template' => $opt{template} } );
+ my $error = $self->print( $opt{'template'} );
die $error if $error;
}
}
+ # at the time we do this, also migrate paytype into cust_pay_batch
+ # so that batches that are open before the migration can still be
+ # processed
+ my @cust_pay_batch = qsearch('cust_pay_batch', {
+ 'custnum' => $cust_main->custnum,
+ 'payby' => 'CHEK',
+ 'paytype' => '',
+ });
+ foreach my $cust_pay_batch (@cust_pay_batch) {
+ $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
+ my $error = $cust_pay_batch->replace;
+ die "$error (setting cust_pay_batch.paytype)" if $error;
+ }
+
$cust_main->complimentary('Y') if $cust_main->payby eq 'COMP';
$cust_main->invoice_attn( $cust_main->payname )