X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=01a47d0d26e88d675a29e73c26bc4b04ed0ed595;hb=c112a7e56557acce5a54d3b57e4cdbab7c43ecc5;hp=3ad7f99bb8e4f70912da368072a9ee0fe29cfddb;hpb=76a7bc00ed44a8123a5ceb0b57f7b9ad05ae82c4;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 3ad7f99bb..01a47d0d2 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -274,6 +274,10 @@ Allow self-service editing of ticket subjects, empty or 'Y' Do not call, empty or 'Y' +=item invoice_ship_address + +Display ship_address ("Service address") on invoices for this customer, empty or 'Y' + =back =head1 METHODS @@ -537,6 +541,33 @@ sub insert { } + warn " setting contacts\n" + if $DEBUG > 1; + + if ( my $contact = delete $options{'contact'} ) { + + foreach my $c ( @$contact ) { + $c->custnum($self->custnum); + my $error = $c->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + } + + } elsif ( my $contact_params = delete $options{'contact_params'} ) { + + my $error = $self->process_o2m( 'table' => 'contact', + 'fields' => FS::contact->cgi_contact_fields, + 'params' => $contact_params, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + warn " setting cust_main_exemption\n" if $DEBUG > 1; @@ -1784,6 +1815,7 @@ sub check { || $self->ut_flag('invoice_noemail') || $self->ut_flag('message_noemail') || $self->ut_enum('locale', [ '', FS::Locales->locales ]) + || $self->ut_flag('invoice_ship_address') ; foreach (qw(company ship_company)) { @@ -1819,6 +1851,11 @@ sub check { $self->ss("$1-$2-$3"); } + #turn off invoice_ship_address if ship & bill are the same + if ($self->bill_locationnum eq $self->ship_locationnum) { + $self->invoice_ship_address(''); + } + # cust_main_county verification now handled by cust_location check $error = @@ -2084,6 +2121,21 @@ sub check { $self->SUPER::check; } +=item replace_check + +Additional checks for replace only. + +=cut + +sub replace_check { + my ($new,$old) = @_; + #preserve old value if global config is set + if ($old && $conf->exists('invoice-ship_address')) { + $new->invoice_ship_address($old->invoice_ship_address); + } + return ''; +} + =item addr_fields Returns a list of fields which have ship_ duplicates. @@ -2346,8 +2398,8 @@ Returns all notes (see L) for this customer. sub notes { my($self,$orderby_classnum) = (shift,shift); - my $orderby = "_DATE DESC"; - $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum; + my $orderby = "sticky DESC, _date DESC"; + $orderby = "classnum ASC, $orderby" if $orderby_classnum; qsearch( 'cust_main_note', { 'custnum' => $self->custnum }, '', @@ -3467,6 +3519,7 @@ sub charge { my ( $setuptax, $taxclass ); #internal taxes my ( $taxproduct, $override ); #vendor (CCH) taxes my $no_auto = ''; + my $separate_bill = ''; my $cust_pkg_ref = ''; my ( $bill_now, $invoice_terms ) = ( 0, '' ); my $locationnum; @@ -3489,7 +3542,8 @@ sub charge { $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : ''; $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : ''; $locationnum = $_[0]->{locationnum} || $self->ship_locationnum; - } else { + $separate_bill = $_[0]->{separate_bill} || ''; + } else { # yuck $amount = shift; $setup_cost = ''; $quantity = 1; @@ -3557,6 +3611,7 @@ sub charge { 'quantity' => $quantity, 'start_date' => $start_date, 'no_auto' => $no_auto, + 'separate_bill' => $separate_bill, 'locationnum'=> $locationnum, } ); @@ -4247,6 +4302,27 @@ sub cust_status { } } +=item is_status_delay_cancel + +Returns true if customer status is 'suspended' +and all suspended cust_pkg return true for +cust_pkg->is_status_delay_cancel. + +This is not a real status, this only meant for hacking display +values, because otherwise treating the customer as suspended is +really the whole point of the delay_cancel option. + +=cut + +sub is_status_delay_cancel { + my ($self) = @_; + return 0 unless $self->status eq 'suspended'; + foreach my $cust_pkg ($self->ncancelled_pkgs) { + return 0 unless $cust_pkg->is_status_delay_cancel; + } + return 1; +} + =item ucfirst_cust_status =item ucfirst_status @@ -4361,6 +4437,179 @@ my ($self,$field) = @_; } +=item payment_history + +Returns an array of hashrefs standardizing information from cust_bill, cust_pay, +cust_credit and cust_refund objects. Each hashref has the following fields: + +I - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous' + +I - value of _date field, unix timestamp + +I - user-friendly date + +I - user-friendly description of item + +I - impact of item on user's balance +(positive for Invoice/Refund/Line item, negative for Payment/Credit.) +Not to be confused with the native 'amount' field in cust_credit, see below. + +I - includes money char + +I - customer balance, chronologically as of this item + +I - includes money char + +I - amount charged for cust_bill (Invoice or Line item) records, undef for other types + +I - amount paid for cust_pay records, undef for other types + +I - amount credited for cust_credit records, undef for other types. +Literally the 'amount' field from cust_credit, renamed here to avoid confusion. + +I - amount refunded for cust_refund records, undef for other types + +The four table-specific keys always have positive values, whether they reflect charges or payments. + +The following options may be passed to this method: + +I - if true, returns charges ('Line item') rather than invoices + +I - unix timestamp, only include records on or after. +If specified, an item of type 'Previous' will also be included. +It does not have table-specific fields. + +I - unix timestamp, only include records before + +I - order from newest to oldest (default is oldest to newest) + +I - optional already-loaded FS::Conf object. + +=cut + +# Caution: this gets used by FS::ClientAPI::MyAccount::billing_history, +# and also for sending customer statements, which should both be kept customer-friendly. +# If you add anything that shouldn't be passed on through the API or exposed +# to customers, add a new option to include it, don't include it by default +sub payment_history { + my $self = shift; + my $opt = ref($_[0]) ? $_[0] : { @_ }; + + my $conf = $$opt{'conf'} || new FS::Conf; + my $money_char = $conf->config("money_char") || '$', + + #first load entire history, + #need previous to calculate previous balance + #loading after end_date shouldn't hurt too much? + my @history = (); + if ( $$opt{'line_items'} ) { + + foreach my $cust_bill ( $self->cust_bill ) { + + push @history, { + 'type' => 'Line item', + 'description' => $_->desc( $self->locale ). + ( $_->sdate && $_->edate + ? ' '. time2str('%d-%b-%Y', $_->sdate). + ' To '. time2str('%d-%b-%Y', $_->edate) + : '' + ), + 'amount' => sprintf('%.2f', $_->setup + $_->recur ), + 'charged' => sprintf('%.2f', $_->setup + $_->recur ), + 'date' => $cust_bill->_date, + 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), + } + foreach $cust_bill->cust_bill_pkg; + + } + + } else { + + push @history, { + 'type' => 'Invoice', + 'description' => 'Invoice #'. $_->display_invnum, + 'amount' => sprintf('%.2f', $_->charged ), + 'charged' => sprintf('%.2f', $_->charged ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_bill; + + } + + push @history, { + 'type' => 'Payment', + 'description' => 'Payment', #XXX type + 'amount' => sprintf('%.2f', 0 - $_->paid ), + 'paid' => sprintf('%.2f', $_->paid ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_pay; + + push @history, { + 'type' => 'Credit', + 'description' => 'Credit', #more info? + 'amount' => sprintf('%.2f', 0 -$_->amount ), + 'credit' => sprintf('%.2f', $_->amount ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_credit; + + push @history, { + 'type' => 'Refund', + 'description' => 'Refund', #more info? type, like payment? + 'amount' => $_->refund, + 'refund' => $_->refund, + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_refund; + + #put it all in chronological order + @history = sort { $a->{'date'} <=> $b->{'date'} } @history; + + #calculate balance, filter items outside date range + my $previous = 0; + my $balance = 0; + my @out = (); + foreach my $item (@history) { + last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'}); + $balance += $$item{'amount'}; + if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) { + $previous += $$item{'amount'}; + next; + } + $$item{'balance'} = sprintf("%.2f",$balance); + foreach my $key ( qw(amount balance) ) { + $$item{$key.'_pretty'} = $$item{$key}; + $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/; + } + push(@out,$item); + } + + # start with previous balance, if there was one + if ($previous) { + my $item = { + 'type' => 'Previous', + 'description' => 'Previous balance', + 'amount' => sprintf("%.2f",$previous), + 'balance' => sprintf("%.2f",$previous), + }; + #false laziness with above + foreach my $key ( qw(amount balance) ) { + $$item{$key.'_pretty'} = $$item{$key}; + $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/; + } + unshift(@out,$item); + } + + @out = reverse @history if $$opt{'reverse_sort'}; + + return @out; +} + =back =head1 CLASS METHODS @@ -4818,7 +5067,7 @@ sub notify { return unless $conf->exists($template); - my $from = $conf->config('invoice_from', $self->agentnum) + my $from = $conf->invoice_from_full($self->agentnum) if $conf->exists('invoice_from', $self->agentnum); $from = $options{from} if exists($options{from});