diff options
104 files changed, 2378 insertions, 460 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 03280c484..16bbaad60 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -4044,7 +4044,7 @@ and customer address. Include units.', 'type' => 'select', 'multiple' => 1, 'select_hash' => [ - #'address1' => 'Billing address', + 'address' => 'Billing or service address', ], }, @@ -4163,6 +4163,13 @@ and customer address. Include units.', }, { + 'key' => 'previous_balance-payments_since', + 'section' => 'invoicing', + 'description' => 'Instead of showing payments (and credits) applied to the invoice, show those received since the previous invoice date.', + 'type' => 'checkbox', + }, + + { 'key' => 'balance_due_below_line', 'section' => 'invoicing', 'description' => 'Place the balance due message below a line. Only meaningful when when invoice_sections is false.', @@ -4182,8 +4189,9 @@ and customer address. Include units.', 'description' => 'Method for standardizing customer addresses.', 'type' => 'select', 'select_hash' => [ '' => '', - 'usps' => 'U.S. Postal Service', + 'usps' => 'U.S. Postal Service', 'ezlocate' => 'EZLocate', + 'tomtom' => 'TomTom', ], }, @@ -4202,6 +4210,13 @@ and customer address. Include units.', }, { + 'key' => 'tomtom-userid', + 'section' => 'UI', + 'description' => 'TomTom geocoding service API key. See <a href="http://www.tomtom.com/">the TomTom website</a> to obtain a key. This is recommended for addresses in the United States only.', + 'type' => 'text', + }, + + { 'key' => 'ezlocate-userid', 'section' => 'UI', 'description' => 'User ID for EZ-Locate service. See <a href="http://www.geocode.com/">the TomTom website</a> for access and pricing information.', @@ -5505,6 +5520,13 @@ and customer address. Include units.', 'type' => 'text', }, + { + 'key' => 'allow_invalid_cards', + 'section' => '', + 'description' => 'Accept invalid credit card numbers. Useful for testing with fictitious customers. There is no good reason to enable this in production.', + 'type' => 'checkbox', + }, + { key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, { key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, { key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" }, diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index de65b44a9..191ff8537 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -38,15 +38,15 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company' => 'custnum | Status | Last, First | Company', - 'Cust. Status | (bill) Customer | (service) Customer' => - 'Status | Last, First or Company (Last, First) | (same for service contact if present)', - 'Cust# | Cust. Status | (bill) Customer | (service) Customer' => - 'custnum | Status | Last, First or Company (Last, First) | (same for service contact if present)', + 'Cust. Status | Customer' => + 'Status | Last, First or Company (Last, First)', + 'Cust# | Cust. Status | Customer' => + 'custnum | Status | Last, First or Company (Last, First)', - 'Cust. Status | (bill) Name | (bill) Company | (service) Name | (service) Company' => - 'Status | Last, First | Company | (same for service contact if present)', - 'Cust# | Cust. Status | (bill) Name | (bill) Company | (service) Name | (service) Company' => - 'custnum | Status | Last, First | Company | (same for service contact if present)', + 'Cust. Status | Name | Company' => + 'Status | Last, First | Company', + 'Cust# | Cust. Status | Name | Company' => + 'custnum | Status | Last, First | Company', 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Invoicing email(s)' => 'custnum | Status | Last, First | Company | (address) | Day phone | Night phone | Invoicing email(s)', @@ -57,13 +57,13 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s) | Payment Type | Current Balance', - 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | Invoicing email(s)' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Day phone | Night phone | Invoicing email(s)' => 'custnum | Status | Last, First | Company | (address) | Day phone | Night phone | (service address) | Invoicing email(s)', - 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (bill) Fax number | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | (service) Fax number | Invoicing email(s) | Payment Type' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type', - 'Cust# | Cust. Status | (bill) Name | (bill) Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Day phone | (bill) Night phone | (bill) Fax number | (service) Name | (service) Company | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Day phone | (service) Night phone | (service) Fax number | Invoicing email(s) | Payment Type | Current Balance' => + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance', 'Invoicing email(s)' => 'Invoicing email(s)', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 780e3ffaf..1215ca414 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -353,6 +353,9 @@ if ( -e $addl_handler_use_file ) { use FS::sales_pkg_class; use FS::svc_alarm; use FS::cable_model; + use FS::invoice_mode; + use FS::invoice_conf; + use FS::cable_provider; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm index a93d98f93..b5cc325d1 100644 --- a/FS/FS/Misc/Geo.pm +++ b/FS/FS/Misc/Geo.pm @@ -10,6 +10,7 @@ use HTML::TokeParser; use URI::Escape 3.31; use Data::Dumper; use FS::Conf; +use Locale::Country; FS::UID->install_callback( sub { $conf = new FS::Conf; @@ -410,6 +411,155 @@ sub standardize_ezlocate { \%result; } +sub standardize_tomtom { + # post-2013 TomTom API + # much better, but incompatible with ezlocate + my $self = shift; + my $location = shift; + my $class = 'Geo::TomTom::Geocoding'; + eval "use $class"; + die $@ if $@; + + my $key = $conf->config('tomtom-userid') + or die "no tomtom-userid configured\n"; + + my $country = code2country($location->{country}); + my ($address1, $address2) = ($location->{address1}, $location->{address2}); + # try to fix some cases of the address fields being switched + if ( $address2 =~ /^\d/ and $address1 !~ /^\d/ ) { + $address2 = $address1; + $address1 = $location->{address2}; + } + my $result = $class->query( + key => $key, + T => $address1, + L => $location->{city}, + AA => $location->{state}, + PC => $location->{zip}, + CC => country2code($country, LOCALE_CODE_ALPHA_3), + ); + unless ( $result->is_success ) { + die "TomTom geocoding error: ".$result->message."\n"; + } + my ($match) = $result->locations; + if (!$match) { + die "Location not found.\n"; + } + my $type = $match->{type}; + warn "tomtom returned $type match\n" if $DEBUG; + warn Dumper($match) if $DEBUG > 1; + my $tract = ''; + if ( defined $match->{censusTract} ) { + $tract = $match->{censusStateCode}. $match->{censusFipsCountyCode}. + join('.', $match->{censusTract} =~ /(....)(..)/); + } + # match levels below "intersection" should not be considered clean + my $clean = ($type eq 'addresspoint' || + $type eq 'poi' || + $type eq 'house' || + $type eq 'intersection' + ) ? 'Y' : ''; + + $address2 = normalize_address2($address2, $location->{country}); + + $address1 = ''; + $address1 = $match->{houseNumber} . ' ' if length($match->{houseNumber}); + $address1 .= $match->{street} if $match->{street}; + + return +{ + address1 => $address1, + address2 => $address2, + city => $match->{city}, + state => $location->{state}, # this will never change + country => $location->{country}, # ditto + zip => ($match->{standardPostalCode} || $match->{postcode}), + latitude => $match->{latitude}, + longitude => $match->{longitude}, + censustract => $tract, + addr_clean => $clean, + }; +} + +=iten normalize_address2 STRING, COUNTRY + +Given an 'address2' STRING, normalize it for COUNTRY postal standards. +Currently only works for US and CA. + +=cut + +# XXX really ought to be a separate module +my %address2_forms = ( + # Postal Addressing Standards, Appendix C + # (plus correction of "hanger" to "hangar") + US => {qw( + APARTMENT APT + BASEMENT BSMT + BUILDING BLDG + DEPARTMENT DEPT + FLOOR FL + FRONT FRNT + HANGAR HNGR + HANGER HNGR + KEY KEY + LOBBY LBBY + LOT LOT + LOWER LOWR + OFFICE OFC + PENTHOUSE PH + PIER PIER + REAR REAR + ROOM RM + SIDE SIDE + SLIP SLIP + SPACE SPC + STOP STOP + SUITE STE + TRAILER TRLR + UNIT UNIT + UPPER UPPR + )}, + # Canada Post Addressing Guidelines 4.3 + CA => {qw( + APARTMENT APT + APPARTEMENT APP + BUREAU BUREAU + SUITE SUITE + UNIT UNIT + UNITÉ UNITÉ + )}, +); + +sub normalize_address2 { + # Some things seen in the address2 field: + # Whitespace + # The complete address (with address1 containing part of the company name, + # or an ATTN or DBA line, or P.O. Box, or department name, or building/suite + # number, etc.) + my ($addr2, $country) = @_; + $addr2 = uc($addr2); + if ( exists($address2_forms{$country}) ) { + my $dict = $address2_forms{$country}; + # protect this + $addr2 =~ s/#\s*(\d)/NUMBER$1/; # /g? + my @words; + # remove all punctuation and spaces + foreach my $w (split(/\W+/, $addr2)) { + if ( exists($dict->{$w}) ) { + push @words, $dict->{$w}; + } else { + push @words, $w; + } + } + my $result = join(' ', @words); + # correct spacing of pound sign + number + $result =~ s/NUMBER(\d)/# $1/; + warn "normalizing '$addr2' to '$result'\n" if $DEBUG > 1; + $addr2 = $result; + } + $addr2; +} + + =back =cut diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index fd035249b..71eddc1eb 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -3038,13 +3038,8 @@ Checks to see if the string is encrypted and returns true or false (1/0) to indi sub is_encrypted { my ($self, $value) = @_; - # Possible Bug - Some work may be required here.... - - if ($value =~ /^M/ && length($value) > 80) { - return 1; - } else { - return 0; - } + # could be more precise about it, but this will do for now + $value =~ /^M/ && length($value) > 80; } =item decrypt($value) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index ed3790452..e44b74edc 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -4272,6 +4272,8 @@ sub tables_hashref { 'svc_cable' => { 'columns' => [ 'svcnum', 'int', '', '', '', '', + 'providernum', 'int', 'NULL', '', '', '', + # XXX "Circuit ID/Order number" 'modelnum', 'int', 'NULL', '', '', '', 'serialnum', 'varchar', 'NULL', $char_d, '', '', 'mac_addr', 'varchar', 'NULL', 12, '', '', @@ -4292,6 +4294,17 @@ sub tables_hashref { 'index' => [], }, + 'cable_provider' => { + 'columns' => [ + 'providernum', 'serial', '', '', '', '', + 'provider', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'providernum', + 'unique' => [ [ 'provider' ], ], + 'index' => [], + }, + 'vend_main' => { 'columns' => [ 'vendnum', 'serial', '', '', '', '', @@ -4377,6 +4390,52 @@ sub tables_hashref { 'index' => [ [ 'derivenum', ], ], }, + 'invoice_mode' => { + 'columns' => [ + 'modenum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'modename', 'varchar', '', 32, '', '', + ], + 'primary_key' => 'modenum', + 'unique' => [ ], + 'index' => [ ], + }, + + 'invoice_conf' => { + 'columns' => [ + 'confnum', 'serial', '', '', '', '', + 'modenum', 'int', '', '', '', '', + 'locale', 'varchar', 'NULL', 16, '', '', + 'notice_name', 'varchar', 'NULL', 64, '', '', + 'subject', 'varchar', 'NULL', 64, '', '', + 'htmlnotes', 'text', 'NULL', '', '', '', + 'htmlfooter', 'text', 'NULL', '', '', '', + 'htmlsummary', 'text', 'NULL', '', '', '', + 'htmlreturnaddress', 'text', 'NULL', '', '', '', + 'latexnotes', 'text', 'NULL', '', '', '', + 'latexfooter', 'text', 'NULL', '', '', '', + 'latexsummary', 'text', 'NULL', '', '', '', + 'latexcoupon', 'text', 'NULL', '', '', '', + 'latexsmallfooter', 'text', 'NULL', '', '', '', + 'latexreturnaddress', 'text', 'NULL', '', '', '', + 'latextopmargin', 'varchar', 'NULL', 16, '', '', + 'latexheadsep', 'varchar', 'NULL', 16, '', '', + 'latexaddresssep', 'varchar', 'NULL', 16, '', '', + 'latextextheight', 'varchar', 'NULL', 16, '', '', + 'latexextracouponspace','varchar', 'NULL', 16, '', '', + 'latexcouponfootsep', 'varchar', 'NULL', 16, '', '', + 'latexcouponamountenclosedsep', 'varchar', 'NULL', 16, '', '', + 'latexcoupontoaddresssep', 'varchar', 'NULL', 16, '', '', + 'latexverticalreturnaddress', 'char', 'NULL', 1, '', '', + 'latexcouponaddcompanytoaddress', 'char', 'NULL', 1, '', '', + 'logo_png', 'blob', 'NULL', '', '', '', + 'logo_eps', 'blob', 'NULL', '', '', '', + 'lpr', 'varchar', 'NULL', $char_d, '', '', + ], + 'primary_key' => 'confnum', + 'unique' => [ [ 'modenum', 'locale' ] ], + 'index' => [ ], + }, # name type nullability length default local diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index db3885443..840df7558 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -18,6 +18,7 @@ use FS::Record qw( qsearch qsearchs ); use FS::Misc qw( generate_ps generate_pdf ); use FS::pkg_category; use FS::pkg_class; +use FS::invoice_mode; use FS::L10N; $DEBUG = 0; @@ -30,12 +31,51 @@ FS::UID->install_callback( sub { $date_format_long = $conf->config('date_format_long') || '%b %o, %Y'; } ); -=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ] +=item conf [ MODE ] + +Returns a configuration handle (L<FS::Conf>) set to the customer's locale. + +If the "mode" pseudo-field is set on the object, the configuration handle +will be an L<FS::invoice_conf> for that invoice mode (and the customer's +locale). + +=cut + +sub conf { + my $self = shift; + my $mode = $self->get('mode'); + if ($self->{_conf} and !defined($mode)) { + return $self->{_conf}; + } + + my $cust_main = $self->cust_main; + my $locale = $cust_main ? $cust_main->locale : ''; + my $conf; + if ( $mode ) { + if ( ref $mode and $mode->isa('FS::invoice_mode') ) { + $mode = $mode->modenum; + } elsif ( $mode =~ /\D/ ) { + die "invalid invoice mode $mode"; + } + $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale }); + if (!$conf) { + $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' }); + # it doesn't have a locale, but system conf still might + $conf->set('locale' => $locale) if $conf; + } + } + # if $mode is unspecified, or if there is no invoice_conf matching this mode + # and locale, then use the system config only (but with the locale) + $conf ||= FS::Conf->new({ 'locale' => $locale }); + # cache it + return $self->{_conf} = $conf; +} + +=item print_text OPTIONS Returns an text invoice, as a list of lines. -Options can be passed as a hashref (recommended) or as a list of time, template -and then any key/value pairs for any other options. +Options can be passed as a hash. I<time>, if specified, is used to control the printing of overdue messages. The default is now. It isn't the date of the invoice; that's the `_date' field. @@ -50,25 +90,19 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume sub print_text { my $self = shift; - my( $today, $template, %opt ); + my %params; if ( ref($_[0]) ) { - %opt = %{ shift() }; - $today = delete($opt{'time'}) || ''; - $template = delete($opt{template}) || ''; + %params = %{ shift() }; } else { - ( $today, $template, %opt ) = @_; + %params = @_; } - my %params = ( 'format' => 'template' ); - $params{'time'} = $today if $today; - $params{'template'} = $template if $template; - $params{$_} = $opt{$_} - foreach grep $opt{$_}, qw( unsquelch_cdr notice_name ); + $params{'format'} = 'template'; # for some reason $self->print_generic( %params ); } -=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ] +=item print_latex HASHREF Internal method - returns a filename of a filled-in LaTeX template for this invoice (Note: add ".tex" to get the actual filename), and a filename of @@ -76,15 +110,16 @@ an associated logo (with the .eps extension included). See print_ps and print_pdf for methods that return PostScript and PDF output. -Options can be passed as a hashref (recommended) or as a list of time, template -and then any key/value pairs for any other options. +Options can be passed as a hash. I<time>, if specified, is used to control the printing of overdue messages. The default is now. It isn't the date of the invoice; that's the `_date' field. It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion functions. -I<template>, if specified, is the name of a suffix for alternate invoices. +I<template>, if specified, is the name of a suffix for alternate invoices. +This is strongly deprecated; see L<FS::invoice_conf> for the right way to +customize invoice templates for different purposes. I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) @@ -92,22 +127,20 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume sub print_latex { my $self = shift; - my $conf = $self->conf; - my( $today, $template, %opt ); + my %params; + if ( ref($_[0]) ) { - %opt = %{ shift() }; - $today = delete($opt{'time'}) || ''; - $template = delete($opt{template}) || ''; + %params = %{ shift() }; } else { - ( $today, $template, %opt ) = @_; + %params = @_; } - my %params = ( 'format' => 'latex' ); - $params{'time'} = $today if $today; - $params{'template'} = $template if $template; - $params{$_} = $opt{$_} - foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number ); + $params{'format'} = 'latex'; + my $conf = $self->conf; + # this needs to go away + my $template = $params{'template'}; + # and this especially $template ||= $self->_agent_template if $self->can('_agent_template'); @@ -191,7 +224,8 @@ Non optional options include Optional options include -template - a value used as a suffix for a configuration template +template - a value used as a suffix for a configuration template. Please +don't use this. time - a value used to control the printing of overdue messages. The default is now. It isn't the date of the invoice; that's the `_date' field. @@ -214,6 +248,7 @@ locale - override customer's locale sub print_generic { my( $self, %params ) = @_; my $conf = $self->conf; + my $today = $params{today} ? $params{today} : time; warn "$me print_generic called on $self with suffix $params{template}\n" if $DEBUG; @@ -227,6 +262,8 @@ sub print_generic { unless $cust_main->payname && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/; + my $locale = $params{'locale'} || $cust_main->locale; + my %delimiters = ( 'latex' => [ '[@--', '--@]' ], 'html' => [ '<%=', '%>' ], 'template' => [ '{', '}' ], @@ -235,11 +272,18 @@ sub print_generic { warn "$me print_generic creating template\n" if $DEBUG > 1; + # set the notice name here, and nowhere else. + my $notice_name = $params{notice_name} + || $conf->config('notice_name') + || $self->notice_name; + #create the template my $template = $params{template} ? $params{template} : $self->_agent_template; my $templatefile = $self->template_conf. $format; $templatefile .= "_$template" if length($template) && $conf->exists($templatefile."_$template"); + + # the base template my @invoice_template = map "$_\n", $conf->config($templatefile) or die "cannot load config data $templatefile"; @@ -380,6 +424,7 @@ sub print_generic { # generate template variables my $returnaddress; + if ( defined( $conf->config_orbase( "invoice_${format}returnaddress", $template @@ -457,7 +502,7 @@ sub print_generic { 'today' => time2str($date_format_long, $today), 'terms' => $self->terms, 'template' => $template, #params{'template'}, - 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function? + 'notice_name' => $notice_name, # escape? 'current_charges' => sprintf("%.2f", $self->charged), 'duedate' => $self->due_date2str($rdate_format), #date_format? @@ -499,7 +544,7 @@ sub print_generic { ); #localization - my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale ); + my $lh = FS::L10N->get_handle( $locale ); $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) }; my %info = FS::Locales->locale_info($cust_main->locale || 'en_US'); # eval to avoid death for unimplemented languages @@ -608,23 +653,12 @@ sub print_generic { # summary formats $invoice_data{'last_bill'} = {}; - # returns the last unpaid bill, not the last bill - #my $last_bill = $pr_cust_bill[-1]; - if ( $self->custnum && $self->invnum ) { - # THIS returns the customer's last bill before this one - my $last_bill = qsearchs({ - 'table' => 'cust_bill', - 'hashref' => { 'custnum' => $self->custnum, - 'invnum' => { op => '<', value => $self->invnum }, - }, - 'order_by' => ' ORDER BY invnum DESC LIMIT 1' - }); - if ( $last_bill ) { + if ( $self->previous_bill ) { + my $last_bill = $self->previous_bill; $invoice_data{'last_bill'} = { '_date' => $last_bill->_date, #unformatted - # all we need for now }; my (@payments, @credits); # for formats that itemize previous payments @@ -766,6 +800,7 @@ sub print_generic { my $taxtotal = 0; my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'), 'subtotal' => $taxtotal, # adjusted below + 'tax_section' => 1, }; my $tax_weight = _pkg_category($tax_section->{description}) ? _pkg_category($tax_section->{description})->weight @@ -952,9 +987,6 @@ sub print_generic { foreach my $section (@sections, @$late_sections) { - warn "$me adding section \n". Dumper($section) - if $DEBUG > 1; - # begin some normalization $section->{'subtotal'} = $section->{'amount'} if $multisection @@ -1167,7 +1199,7 @@ sub print_generic { $adjust_section->{'pretotal'} = $self->mt('New charges total').' '. $other_money_char. sprintf('%.2f', $self->charged ); } - }else{ + } else { push @total_items, $total; } push @buf,['','-----------']; @@ -1554,12 +1586,9 @@ sub print_html { my %params; if ( ref($_[0]) ) { %params = %{ shift() }; - }else{ - $params{'time'} = shift; - $params{'template'} = shift; - $params{'cid'} = shift; + } else { + %params = @_; } - $params{'format'} = 'html'; $self->print_generic( %params ); diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index c1c69fa3f..fa54e0bbd 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -342,6 +342,21 @@ sub _upgrade_data { or die $dbh->errstr; $cve_2013_3373_sth->execute or die $cve_2013_3373_sth->errstr; + # Remove dangling customer links, if any + my %target_pkey = ('cust_main' => 'custnum', 'cust_svc' => 'svcnum'); + for my $table (keys %target_pkey) { + my $pkey = $target_pkey{$table}; + my $rows = $dbh->do( + "DELETE FROM links WHERE id IN(". + "SELECT links.id FROM links LEFT JOIN $table ON (links.target = ". + "'freeside://freeside/$table/' || $table.$pkey) ". + "WHERE links.target like 'freeside://freeside/$table/%' ". + "AND $table.$pkey IS NULL". + ")" + ) or die $dbh->errstr; + warn "Removed $rows dangling ticket-$table links\n" if $rows > 0; + } + return; } diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index ccba1de3a..d7f998bdf 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -229,18 +229,25 @@ sub cust_header { 'Cust#' => 'custnum', 'Name' => 'contact', 'Company' => 'company', + + # obsolete but might still be referenced in configuration '(bill) Customer' => 'name', '(service) Customer' => 'ship_name', '(bill) Name' => 'contact', '(service) Name' => 'ship_contact', '(bill) Company' => 'company', '(service) Company' => 'ship_company', + '(bill) Day phone' => 'daytime', + '(bill) Night phone' => 'night', + '(bill) Fax number' => 'fax', + + 'Customer' => 'name', 'Address 1' => 'bill_address1', 'Address 2' => 'bill_address2', 'City' => 'bill_city', 'State' => 'bill_state', 'Zip' => 'bill_zip', - 'Country' => 'country_full', + 'Country' => 'bill_country_full', 'Day phone' => 'daytime', # XXX should use msgcat, but how? 'Night phone' => 'night', # XXX should use msgcat, but how? 'Fax number' => 'fax', @@ -249,19 +256,13 @@ sub cust_header { '(bill) City' => 'bill_city', '(bill) State' => 'bill_state', '(bill) Zip' => 'bill_zip', - '(bill) Country' => 'country_full', - '(bill) Day phone' => 'daytime', # XXX should use msgcat, but how? - '(bill) Night phone' => 'night', # XXX should use msgcat, but how? - '(bill) Fax number' => 'fax', + '(bill) Country' => 'bill_country_full', '(service) Address 1' => 'ship_address1', '(service) Address 2' => 'ship_address2', '(service) City' => 'ship_city', '(service) State' => 'ship_state', '(service) Zip' => 'ship_zip', '(service) Country' => 'ship_country_full', - '(service) Day phone' => 'ship_daytime', # XXX should use msgcat, how? - '(service) Night phone' => 'ship_night', # XXX should use msgcat, how? - '(service) Fax number' => 'ship_fax', 'Invoicing email(s)' => 'invoicing_list_emailonly_scalar', 'Payment Type' => 'payby', 'Current Balance' => 'current_balance', @@ -348,8 +349,10 @@ sub cust_sql_fields { } } } - - push @fields, 'payby' if grep { $_ eq 'payby'} @cust_fields; + + foreach my $field (qw(daytime night fax payby)) { + push @fields, $field if (grep { $_ eq $field } @cust_fields); + } push @fields, 'agent_custid'; my @extra_fields = (); diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm index 0cd07ef15..d70ff18b4 100644 --- a/FS/FS/agent.pm +++ b/FS/FS/agent.pm @@ -378,6 +378,23 @@ sub payment_gateway { $payment_gateway; } +=item invoice_modes + +Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e. +those with this agentnum or null agentnum). + +=cut + +sub invoice_modes { + my $self = shift; + qsearch( { + table => 'invoice_mode', + hashref => { agentnum => $self->agentnum }, + extra_sql => ' OR agentnum IS NULL', + order_by => ' ORDER BY modename', + } ); +} + =item num_prospect_cust_main Returns the number of prospects (customers with no packages ever ordered) for diff --git a/FS/FS/cable_provider.pm b/FS/FS/cable_provider.pm new file mode 100644 index 000000000..e988192f4 --- /dev/null +++ b/FS/FS/cable_provider.pm @@ -0,0 +1,112 @@ +package FS::cable_provider; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cable_provider - Object methods for cable_provider records + +=head1 SYNOPSIS + + use FS::cable_provider; + + $record = new FS::cable_provider \%hash; + $record = new FS::cable_provider { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cable_provider object represents a cable service provider. +FS::cable_provider inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item providernum + +primary key + +=item provider + +provider + +=item disabled + +disabled + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new provider. To add the provider to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'cable_provider'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid provider. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('providernum') + || $self->ut_text('provider') + || $self->ut_enum('disabled', [ '', 'Y' ] ) + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index fc6a7ddbe..a747a782d 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -80,7 +80,7 @@ FS::cust_bill - Object methods for cust_bill records $tax_amount = $record->tax; @lines = $cust_bill->print_text; - @lines = $cust_bill->print_text $time; + @lines = $cust_bill->print_text('time' => $time); =head1 DESCRIPTION @@ -153,7 +153,13 @@ Invoices are normally created by calling the bill method of a customer object =cut sub table { 'cust_bill'; } -sub notice_name { 'Invoice'; } + +# should be the ONLY occurrence of "Invoice" in invoice rendering code. +# (except email_subject and invnum_date_pretty) +sub notice_name { + my $self = shift; + $self->conf->config('notice_name') || 'Invoice' +} sub cust_linked { $_[0]->cust_main_custnum; } sub cust_unlinked_msg { @@ -422,6 +428,25 @@ sub display_invnum { } } +=item previous_bill + +Returns the customer's last invoice before this one. + +=cut + +sub previous_bill { + my $self = shift; + if ( !$self->get('previous_bill') ) { + $self->set('previous_bill', qsearchs({ + 'table' => 'cust_bill', + 'hashref' => { 'custnum' => $self->custnum, + '_date' => { op=>'<', value=>$self->_date } }, + 'order_by' => 'ORDER BY _date DESC LIMIT 1', + }) ); + } + $self->get('previous_bill'); +} + =item previous Returns a list consisting of the total previous balance for this customer, @@ -1024,7 +1049,7 @@ Options: sender address, required -=item tempate +=item template alternate template name, optional @@ -1058,15 +1083,10 @@ sub generate_email { my %return = ( 'from' => $args{'from'}, - 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), + 'subject' => ($args{'subject'} || $self->email_subject), ); - my %opt = ( - 'unsquelch_cdr' => $conf->exists('voip-cdr_email'), - 'template' => $args{'template'}, - 'notice_name' => ( $args{'notice_name'} || 'Invoice' ), - 'no_coupon' => $args{'no_coupon'}, - ); + $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email'); my $cust_main = $self->cust_main; @@ -1108,7 +1128,7 @@ sub generate_email { if ( ref($args{'print_text'}) eq 'ARRAY' ) { $data = $args{'print_text'}; } else { - $data = [ $self->print_text(\%opt) ]; + $data = [ $self->print_text(\%args) ]; } } @@ -1165,10 +1185,10 @@ sub generate_email { 'Filename' => 'barcode.png', 'Content-ID' => "<$barcode_content_id>", ; - $opt{'barcode_cid'} = $barcode_content_id; + $args{'barcode_cid'} = $barcode_content_id; } - $htmldata = $self->print_html({ 'cid'=>$content_id, %opt }); + $htmldata = $self->print_html({ 'cid'=>$content_id, %args }); } $alternative->attach( @@ -1230,7 +1250,7 @@ sub generate_email { $related->add_part($image) if $image; - my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt); + my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args); $return{'mimeparts'} = [ $related, $pdf, @otherparts ]; @@ -1262,7 +1282,7 @@ sub generate_email { #mime parts arguments a la MIME::Entity->build(). $return{'mimeparts'} = [ - { $self->mimebuild_pdf(\%opt) } + { $self->mimebuild_pdf(\%args) } ]; } @@ -1282,7 +1302,7 @@ sub generate_email { if ( ref($args{'print_text'}) eq 'ARRAY' ) { $return{'body'} = $args{'print_text'}; } else { - $return{'body'} = [ $self->print_text(\%opt) ]; + $return{'body'} = [ $self->print_text(\%args) ]; } } @@ -1311,105 +1331,48 @@ sub mimebuild_pdf { ); } -=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ] +=item send HASHREF Sends this invoice to the destinations configured for this customer: sends email, prints and/or faxes. See L<FS::cust_main_invoice>. -Options can be passed as a hashref (recommended) or as a list of up to -four values for templatename, agentnum, invoice_from and amount. +Options can be passed as a hashref. Positional parameters are no longer +allowed. -I<template>, if specified, is the name of a suffix for alternate invoices. +I<template>: a suffix for alternate invoices -I<agentnum>, if specified, means that this invoice will only be sent for customers -of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a -single agent) or an arrayref of agentnums. +I<agentnum>: obsolete, now does nothing. -I<invoice_from>, if specified, overrides the default email invoice From: address. +I<invoice_from> overrides the default email invoice From: address. -I<amount>, if specified, only sends the invoice if the total amount owed on this -invoice and all older invoices is greater than the specified amount. +I<amount>: obsolete, does nothing -I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) +I<notice_name> overrides "Invoice" as the name of the sent document +(templates from 10/2009 or newer required). -I<lpr>, if specified, is passed to +I<lpr> overrides the system 'lpr' option as the command to print a document +from standard input. =cut -sub queueable_send { - my %opt = @_; - - my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } ) - or die "invalid invoice number: " . $opt{invnum}; - - my @args = ( $opt{template}, $opt{agentnum} ); - push @args, $opt{invoice_from} - if exists($opt{invoice_from}) && $opt{invoice_from}; - - my $error = $self->send( @args ); - die $error if $error; - -} - sub send { my $self = shift; + my $opt = ref($_[0]) ? $_[0] : +{ @_ }; my $conf = $self->conf; - my( $template, $invoice_from, $notice_name ); - my $agentnums = ''; - my $balance_over = 0; - my $lpr = ''; - - if ( ref($_[0]) ) { - my $opt = shift; - $template = $opt->{'template'} || ''; - if ( $agentnums = $opt->{'agentnum'} ) { - $agentnums = [ $agentnums ] unless ref($agentnums); - } - $invoice_from = $opt->{'invoice_from'}; - $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'}; - $notice_name = $opt->{'notice_name'}; - $lpr = $opt->{'lpr'} - } else { - $template = scalar(@_) ? shift : ''; - if ( scalar(@_) && $_[0] ) { - $agentnums = ref($_[0]) ? shift : [ shift ]; - } - $invoice_from = shift if scalar(@_); - $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/; - } - my $cust_main = $self->cust_main; - return 'N/A' unless ! $agentnums - or grep { $_ == $cust_main->agentnum } @$agentnums; - - return '' - unless $cust_main->total_owed_date($self->_date) > $balance_over; - - $invoice_from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from', $cust_main->agentnum ); - - my %opt = ( - 'template' => $template, - 'invoice_from' => $invoice_from, - 'notice_name' => ( $notice_name || 'Invoice' ), - ); - my @invoicing_list = $cust_main->invoicing_list; - #$self->email_invoice(\%opt) - $self->email(\%opt) + $self->email($opt) if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) && ! $self->invoice_noemail; - $opt{'lpr'} = $lpr; - #$self->print_invoice(\%opt) - $self->print(\%opt) + $self->print($opt) if grep { $_ eq 'POST' } @invoicing_list; #postal #this has never been used post-$ORIGINAL_ISP afaik - $self->fax_invoice(\%opt) + $self->fax_invoice($opt) if grep { $_ eq 'FAX' } @invoicing_list; #fax ''; @@ -1418,16 +1381,17 @@ sub send { =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] -Emails this invoice. +Sends this invoice to the customer's email destination(s). -Options can be passed as a hashref (recommended) or as a list of up to -two values for templatename and invoice_from. +Options must be passed as a hashref. Positional parameters are no longer +allowed. I<template>, if specified, is the name of a suffix for alternate invoices. -I<invoice_from>, if specified, overrides the default email invoice From: address. +I<invoice_from>, if specified, overrides the default email invoice From: +address. -I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required) +I<notice_name> is the name of the sent document. =cut @@ -1437,38 +1401,30 @@ sub queueable_email { my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } ) or die "invalid invoice number: " . $opt{invnum}; - my %args = ( 'template' => $opt{template} ); - $args{$_} = $opt{$_} - foreach grep { exists($opt{$_}) && $opt{$_} } - qw( invoice_from notice_name no_coupon ); + my %args = map {$_ => $opt{$_}} + grep { $opt{$_} } + qw( invoice_from notice_name no_coupon template ); my $error = $self->email( \%args ); die $error if $error; } -#sub email_invoice { sub email { my $self = shift; return if $self->hide; my $conf = $self->conf; - - my( $template, $invoice_from, $notice_name, $no_coupon ); - if ( ref($_[0]) ) { - my $opt = shift; - $template = $opt->{'template'} || ''; - $invoice_from = $opt->{'invoice_from'}; - $notice_name = $opt->{'notice_name'} || 'Invoice'; - $no_coupon = $opt->{'no_coupon'} || 0; - } else { - $template = scalar(@_) ? shift : ''; - $invoice_from = shift if scalar(@_); - $notice_name = 'Invoice'; - $no_coupon = 0; + my $opt = shift; + if ($opt and !ref($opt)) { + die "FS::cust_bill::email called with positional parameters"; } - $invoice_from ||= $self->_agent_invoice_from || #XXX should go away - $conf->config('invoice_from', $self->cust_main->agentnum ); + my $template = $opt->{template}; + my $from = delete $opt->{invoice_from}; + + # this is where we set the From: address + $from ||= $self->_agent_invoice_from || #XXX should go away + $conf->config('invoice_from', $self->cust_main->agentnum ); my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; @@ -1478,20 +1434,19 @@ sub email { die 'No recipients for customer #'. $self->custnum; } else { #default: better to notify this person than silence - @invoicing_list = ($invoice_from); + @invoicing_list = ($from); } } + # this is where we set the Subject: my $subject = $self->email_subject($template); my $error = send_email( $self->generate_email( - 'from' => $invoice_from, + 'from' => $from, 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ], 'subject' => $subject, - 'template' => $template, - 'notice_name' => $notice_name, - 'no_coupon' => $no_coupon, + %$opt, # template, etc. ) ); die "can't email invoice: $error\n" if $error; @@ -1518,12 +1473,12 @@ sub email_subject { eval qq("$subject"); } -=item lpr_data HASHREF | [ TEMPLATE ] +=item lpr_data HASHREF Returns the postscript or plaintext for this invoice as an arrayref. -Options can be passed as a hashref (recommended) or as a single optional value -for template. +Options must be passed as a hashref. Positional parameters are no longer +allowed. I<template>, if specified, is the name of a suffix for alternate invoices. @@ -1534,31 +1489,21 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume sub lpr_data { my $self = shift; my $conf = $self->conf; - my( $template, $notice_name ); - if ( ref($_[0]) ) { - my $opt = shift; - $template = $opt->{'template'} || ''; - $notice_name = $opt->{'notice_name'} || 'Invoice'; - } else { - $template = scalar(@_) ? shift : ''; - $notice_name = 'Invoice'; + my $opt = shift; + if ($opt and !ref($opt)) { + # nobody does this anyway + die "FS::cust_bill::lpr_data called with positional parameters"; } - my %opt = ( - 'template' => $template, - 'notice_name' => $notice_name, - ); - my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text'; - [ $self->$method( \%opt ) ]; + [ $self->$method( $opt ) ]; } -=item print HASHREF | [ TEMPLATE ] +=item print HASHREF Prints this invoice. -Options can be passed as a hashref (recommended) or as a single optional -value for template. +Options must be passed as a hashref. I<template>, if specified, is the name of a suffix for alternate invoices. @@ -1566,48 +1511,34 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume =cut -#sub print_invoice { sub print { my $self = shift; return if $self->hide; my $conf = $self->conf; - - my( $template, $notice_name, $lpr ); - if ( ref($_[0]) ) { - my $opt = shift; - $template = $opt->{'template'} || ''; - $notice_name = $opt->{'notice_name'} || 'Invoice'; - $lpr = $opt->{'lpr'} - } else { - $template = scalar(@_) ? shift : ''; - $notice_name = 'Invoice'; - $lpr = ''; + my $opt = shift; + if ($opt and !ref($opt)) { + die "FS::cust_bill::print called with positional parameters"; } - my %opt = ( - 'template' => $template, - 'notice_name' => $notice_name, - ); - + my $lpr = delete $opt->{lpr}; if($conf->exists('invoice_print_pdf')) { # Add the invoice to the current batch. - $self->batch_invoice(\%opt); + $self->batch_invoice($opt); } else { do_print( - $self->lpr_data(\%opt), + $self->lpr_data($opt), 'agentnum' => $self->cust_main->agentnum, 'lpr' => $lpr, ); } } -=item fax_invoice HASHREF | [ TEMPLATE ] +=item fax_invoice HASHREF Faxes this invoice. -Options can be passed as a hashref (recommended) or as a single optional -value for template. +Options must be passed as a hashref. I<template>, if specified, is the name of a suffix for alternate invoices. @@ -1619,15 +1550,9 @@ sub fax_invoice { my $self = shift; return if $self->hide; my $conf = $self->conf; - - my( $template, $notice_name ); - if ( ref($_[0]) ) { - my $opt = shift; - $template = $opt->{'template'} || ''; - $notice_name = $opt->{'notice_name'} || 'Invoice'; - } else { - $template = scalar(@_) ? shift : ''; - $notice_name = 'Invoice'; + my $opt = shift; + if ($opt and !ref($opt)) { + die "FS::cust_bill::fax_invoice called with positional parameters"; } die 'FAX invoice destination not (yet?) supported with plain text invoices.' @@ -1636,12 +1561,7 @@ sub fax_invoice { my $dialstring = $self->cust_main->getfield('fax'); #Check $dialstring? - my %opt = ( - 'template' => $template, - 'notice_name' => $notice_name, - ); - - my $error = send_fax( 'docdata' => $self->lpr_data(\%opt), + my $error = send_fax( 'docdata' => $self->lpr_data($opt), 'dialstring' => $dialstring, ); die $error if $error; @@ -1730,29 +1650,6 @@ sub spool_invoice { ); } -=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] - -Like B<send>, but only sends the invoice if it is the newest open invoice for -this customer. - -=cut - -sub send_if_newest { - my $self = shift; - - return '' - if scalar( - grep { $_->owed > 0 } - qsearch('cust_bill', { - 'custnum' => $self->custnum, - #'_date' => { op=>'>', value=>$self->_date }, - 'invnum' => { op=>'>', value=>$self->invnum }, - } ) - ); - - $self->send(@_); -} - =item send_csv OPTION => VALUE, ... Sends invoice as a CSV data-file to a remote host with the specified protocol. @@ -3109,12 +3006,25 @@ sub _items_credits { my @b; #credits - foreach ( $self->cust_credited ) { + my @objects; + if ( $self->conf->exists('previous_balance-payments_since') ) { + my $date = 0; + $date = $self->previous_bill->_date if $self->previous_bill; + @objects = qsearch('cust_credit', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $date}, + }); + # hard to do this in the qsearch... + @objects = grep { $_->_date < $self->_date } @objects; + } else { + @objects = $self->cust_credited; + } - #something more elaborate if $_->amount ne $_->cust_credit->credited ? + foreach my $obj ( @objects ) { + my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit; - my $reason = substr($_->cust_credit->reason, 0, $trim_len); - $reason .= '...' if length($reason) < length($_->cust_credit->reason); + my $reason = substr($cust_credit->reason, 0, $trim_len); + $reason .= '...' if length($reason) < length($cust_credit->reason); $reason = " ($reason) " if $reason; push @b, { @@ -3122,8 +3032,8 @@ sub _items_credits { # " (". time2str("%x",$_->cust_credit->_date) .")". # $reason, 'description' => $self->mt('Credit applied').' '. - time2str($date_format,$_->cust_credit->_date). $reason, - 'amount' => sprintf("%.2f",$_->amount), + time2str($date_format,$obj->_date). $reason, + 'amount' => sprintf("%.2f",$obj->amount), }; } @@ -3135,21 +3045,31 @@ sub _items_payments { my $self = shift; my @b; - #get & print payments - foreach ( $self->cust_bill_pay ) { - - #something more elaborate if $_->amount ne ->cust_pay->paid ? + my $detailed = $self->conf->exists('invoice_payment_details'); + my @objects; + if ( $self->conf->exists('previous_balance-payments_since') ) { + my $date = 0; + $date = $self->previous_bill->_date if $self->previous_bill; + @objects = qsearch('cust_pay', { + 'custnum' => $self->custnum, + '_date' => {op => '>=', value => $date}, + }); + @objects = grep { $_->_date < $self->_date } @objects; + } else { + @objects = $self->cust_bill_pay; + } + foreach my $obj (@objects) { + my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay; my $desc = $self->mt('Payment received').' '. - time2str($date_format,$_->cust_pay->_date ); - $desc .= $self->mt(' via ' . $_->cust_pay->payby_payinfo_pretty) - if ( $self->conf->exists('invoice_payment_details') ); - + time2str($date_format, $cust_pay->_date ); + $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty) + if $detailed; + push @b, { 'description' => $desc, - 'amount' => sprintf("%.2f", $_->amount ) + 'amount' => sprintf("%.2f", $obj->amount ) }; - } @b; diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index b98ade157..11e97ecfe 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -10,6 +10,8 @@ use FS::Conf; use FS::prospect_main; use FS::cust_main; use FS::cust_main_county; +use FS::GeocodeCache; +use Date::Format qw( time2str ); $import = 0; @@ -677,6 +679,13 @@ sub process_censustract_update { return; } +=item process_set_coord + +Queueable function to find and fill in coordinates for all locations that +lack them. Because this uses the Google Maps API, it's internally rate +limited and must run in a single process. + +=cut sub process_set_coord { my $job = shift; @@ -716,6 +725,67 @@ sub process_set_coord { return; } +=item process_standardize [ LOCATIONNUMS ] + +Performs address standardization on locations with unclean addresses, +using whatever method you have configured. If the standardize_* method +returns a I<clean> address match, the location will be updated. This is +always an in-place update (because the physical location is the same, +and is just being referred to by a more accurate name). + +Disabled locations will be skipped, as nobody cares. + +If any LOCATIONNUMS are provided, only those locations will be updated. + +=cut + +sub process_standardize { + my $job = shift; + my @others = qsearch('queue', { + 'status' => 'locked', + 'job' => $job->job, + 'jobnum' => {op=>'!=', value=>$job->jobnum}, + }); + return if @others; + my @locationnums = grep /^\d+$/, @_; + my $where = "AND locationnum IN(".join(',',@locationnums).")" + if scalar(@locationnums); + my @locations = qsearch({ + table => 'cust_location', + hashref => { addr_clean => '', disabled => '' }, + extra_sql => $where, + }); + my $n_todo = scalar(@locations); + my $n_done = 0; + + # special: log this + my $log; + eval "use Text::CSV"; + open $log, '>', "$FS::UID::cache_dir/process_standardize-" . + time2str('%Y%m%d',time) . + ".csv"; + my $csv = Text::CSV->new({binary => 1, eol => "\n"}); + + foreach my $cust_location (@locations) { + $job->update_statustext( int(100 * $n_done/$n_todo) . ",$n_done / $n_todo locations" ) if $job; + my $result = FS::GeocodeCache->standardize($cust_location); + if ( $result->{addr_clean} and !$result->{error} ) { + my @cols = ($cust_location->locationnum); + foreach (keys %$result) { + push @cols, $cust_location->get($_), $result->{$_}; + $cust_location->set($_, $result->{$_}); + } + # bypass immutable field restrictions + my $error = $cust_location->FS::Record::replace; + warn "location ".$cust_location->locationnum.": $error\n" if $error; + $csv->print($log, \@cols); + } + $n_done++; + dbh->commit; # so that we can resume if interrupted + } + close $log; +} + =head1 BUGS =head1 SEE ALSO diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 641d54a30..a9a4cb0ef 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -17,6 +17,7 @@ use vars qw( $DEBUG $me $conf @encrypted_fields $import $ignore_expired_card $ignore_banned_card $ignore_illegal_zip + $ignore_invalid_card $skip_fuzzyfiles @paytypes ); @@ -89,6 +90,7 @@ $me = '[FS::cust_main]'; $import = 0; $ignore_expired_card = 0; $ignore_banned_card = 0; +$ignore_invalid_card = 0; $skip_fuzzyfiles = 0; @@ -102,6 +104,7 @@ sub nohistory_fields { ('payinfo', 'paycvv'); } install_callback FS::UID sub { $conf = new FS::Conf; #yes, need it for stuff below (prolly should be cached) + $ignore_invalid_card = $conf->exists('allow_invalid_cards'); }; sub _cache { @@ -1826,7 +1829,8 @@ sub check { # Need some kind of global flag to accept invalid cards, for testing # on scrubbed data. - if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { + if ( !$import && !$ignore_invalid_card && $check_payinfo && + $self->payby =~ /^(CARD|DCRD)$/ ) { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; @@ -1898,7 +1902,8 @@ sub check { $self->payissue(''); } - } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) { + } elsif ( !$ignore_invalid_card && $check_payinfo && + $self->payby =~ /^(CHEK|DCHK)$/ ) { my $payinfo = $self->payinfo; $payinfo =~ s/[^\d\@\.]//g; @@ -4071,6 +4076,16 @@ sub ship_contact_firstlast { # code2country($self->country); #} +sub bill_country_full { + my $self = shift; + code2country($self->bill_location->country); +} + +sub ship_country_full { + my $self = shift; + code2country($self->ship_location->country); +} + =item county_state_county [ PREFIX ] Returns a string consisting of just the county, state and country. @@ -4910,9 +4925,9 @@ sub queueable_print { my %opt = @_; my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } ) - or die "invalid customer number: " . $opt{custvnum}; + or die "invalid customer number: " . $opt{custnum}; - my $error = $self->print( $opt{template} ); + my $error = $self->print( { 'template' => $opt{template} } ); die $error if $error; } @@ -5145,7 +5160,7 @@ sub _upgrade_data { #class method }; - FS::upgrade_journal->set_done('cust_main__trimspaces'); + FS::upgrade_journal->set_done('cust_main__cust_payby'); } $class->_upgrade_otaker(%opts); diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index f14f897ea..182527ff9 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -610,14 +610,24 @@ sub search { ## # address ## - if ( $params->{'address'} =~ /\S/ ) { - my $address = dbh->quote('%'. lc($params->{'address'}). '%'); - push @where, "EXISTS( - SELECT 1 FROM cust_location - WHERE cust_location.custnum = cust_main.custnum - AND (LOWER(cust_location.address1) LIKE $address OR - LOWER(cust_location.address2) LIKE $address) - )"; + if ( $params->{'address'} ) { + # allow this to be an arrayref + my @values = ($params->{'address'}); + @values = @{$values[0]} if ref($values[0]); + my @orwhere; + foreach (grep /\S/, @values) { + my $address = dbh->quote('%'. lc($_). '%'); + push @orwhere, + "LOWER(cust_location.address1) LIKE $address", + "LOWER(cust_location.address2) LIKE $address"; + } + if (@orwhere) { + push @where, "EXISTS( + SELECT 1 FROM cust_location + WHERE cust_location.custnum = cust_main.custnum + AND (".join(' OR ',@orwhere).") + )"; + } } ## diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 212c04e0f..f584b415e 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -131,9 +131,12 @@ linked to a customer. sub country_full { my $self = shift; - $self->cust_linked - ? FS::cust_main::country_full($self) - : $self->cust_unlinked_msg; + if ( $self->locationnum ) { # cust_pkg has this + my $location = FS::cust_location->by_key($self->locationnum); + $location ? $location->country_full : ''; + } elsif ( $self->cust_linked ) { + $self->cust_main->bill_country_full; + } } =item invoicing_list_emailonly diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index a61d67e11..10a007c57 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -244,7 +244,7 @@ are inserted. In addition to calculating the tax for the line items, this will calculate any appropriate tax exemptions and attach them to the line items. -Options may include 'custnum' and 'invoice_date' in case the cust_bill_pkg +Options may include 'custnum' and 'invoice_time' in case the cust_bill_pkg objects belong to an invoice that hasn't been inserted yet. Options may include 'exemptions', an arrayref of L<FS::cust_tax_exempt_pkg> @@ -276,7 +276,7 @@ sub taxline { my $cust_bill = $taxables->[0]->cust_bill; my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'}; - my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{'invoice_date'}; + my $invoice_time = $cust_bill ? $cust_bill->_date : $opt{'invoice_time'}; my $cust_main = FS::cust_main->by_key($custnum) if $custnum > 0; if (!$cust_main) { # better way to handle this? should we just assume that it's taxable? @@ -364,22 +364,36 @@ sub taxline { if ( $self->exempt_amount && $self->exempt_amount > 0 and $taxable_charged > 0 ) { - #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5]; - my ($mon,$year) = - (localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5]; - $mon++; - $year += 1900; - my $freq = $cust_bill_pkg->freq; - unless ($freq) { - $freq = $part_pkg->freq || 1; # less trustworthy fallback - } - if ( $freq !~ /(\d+)$/ ) { - $dbh->rollback if $oldAutoCommit; - return "daily/weekly package definitions not (yet?)". - " compatible with monthly tax exemptions"; + # If the billing period extends across multiple calendar months, + # there may be several months of exemption available. + my $sdate = $cust_bill_pkg->sdate || $invoice_time; + my $start_month = (localtime($sdate))[4] + 1; + my $start_year = (localtime($sdate))[5] + 1900; + my $edate = $cust_bill_pkg->edate || $invoice_time; + my $end_month = (localtime($edate))[4] + 1; + my $end_year = (localtime($edate))[5] + 1900; + + # If the partial last month + partial first month <= one month, + # don't use the exemption in the last month + # (unless the last month is also the first month, e.g. one-time + # charges) + if ( (localtime($sdate))[3] >= (localtime($edate))[3] + and ($start_month != $end_month or $start_year != $end_year) + ) { + $end_month--; + if ( $end_month == 0 ) { + $end_year--; + $end_month = 12; + } } - my $taxable_per_month = - sprintf("%.2f", $taxable_charged / $freq ); + + # number of months of exemption available + my $freq = ($end_month - $start_month) + + ($end_year - $start_year) * 12 + + 1; + + # divide equally among all of them + my $permonth = sprintf('%.2f', $taxable_charged / $freq); #call the whole thing off if this customer has any old #exemption records... @@ -392,9 +406,15 @@ sub taxline { 'run bin/fs-migrate-cust_tax_exempt?'; } - foreach my $which_month ( 1 .. $freq ) { - - #maintain the new exemption table now + my ($mon, $year) = ($start_month, $start_year); + while ($taxable_charged > 0.005 and + ($year < $end_year or + ($year == $end_year and $mon <= $end_month) + ) + ) { + + # find the sum of the exemption used by this customer, for this tax, + # in this month my $sql = " SELECT SUM(amount) FROM cust_tax_exempt_pkg @@ -408,7 +428,7 @@ sub taxline { "; my $sth = dbh->prepare($sql) or do { $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; + return "fatal: can't lookup existing exemption: ". dbh->errstr; }; $sth->execute( $custnum, @@ -417,10 +437,11 @@ sub taxline { $mon, ) or do { $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; + return "fatal: can't lookup existing exemption: ". dbh->errstr; }; my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0; + # add any exemption we're already using for another line item foreach ( grep { $_->taxnum == $self->taxnum && $_->exempt_monthly eq 'Y' && $_->month == $mon && @@ -430,13 +451,15 @@ sub taxline { { $existing_exemption += $_->amount; } - + my $remaining_exemption = $self->exempt_amount - $existing_exemption; if ( $remaining_exemption > 0 ) { - my $addl = $remaining_exemption > $taxable_per_month - ? $taxable_per_month + my $addl = $remaining_exemption > $permonth + ? $permonth : $remaining_exemption; + $addl = $taxable_charged if $addl > $taxable_charged; + push @new_exemptions, FS::cust_tax_exempt_pkg->new({ amount => sprintf('%.2f', $addl), exempt_monthly => 'Y', @@ -445,7 +468,6 @@ sub taxline { }); $taxable_charged -= $addl; } - last if $taxable_charged < 0.005; # if they're using multiple months of exemption for a multi-month # package, then record the exemptions in separate months $mon++; @@ -454,7 +476,7 @@ sub taxline { $year++; } - } #foreach $which_month + } } # if exempt_amount $_->taxnum($self->taxnum) foreach @new_exemptions; diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm index 5914ab5c5..d78c574e4 100644 --- a/FS/FS/cust_payby.pm +++ b/FS/FS/cust_payby.pm @@ -7,9 +7,11 @@ use FS::Record qw( qsearchs ); #qsearch; use FS::payby; use FS::cust_main; use Business::CreditCard qw( validate cardtype ); +use FS::Msgcat qw( gettext ); use vars qw( $conf @encrypted_fields $ignore_expired_card $ignore_banned_card + $ignore_invalid_card ); @encrypted_fields = ('payinfo', 'paycvv'); @@ -17,10 +19,12 @@ sub nohistory_fields { ('payinfo', 'paycvv'); } $ignore_expired_card = 0; $ignore_banned_card = 0; +$ignore_invalid_card = 0; install_callback FS::UID sub { $conf = new FS::Conf; #yes, need it for stuff below (prolly should be cached) + $ignore_invalid_card = $conf->exists('allow_invalid_cards'); }; =head1 NAME @@ -197,7 +201,8 @@ sub check { # Need some kind of global flag to accept invalid cards, for testing # on scrubbed data. #XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { - if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { + if ( !$ignore_invalid_card && + $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) { my $payinfo = $self->payinfo; $payinfo =~ s/\D//g; @@ -269,7 +274,8 @@ sub check { $self->payissue(''); } - } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) { + } elsif ( !$ignore_invalid_card && + $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) { my $payinfo = $self->payinfo; $payinfo =~ s/[^\d\@\.]//g; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 19ef1f326..0cb1b50a2 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2613,14 +2613,30 @@ sub part_pkg_currency_option { =item cust_svc [ OPTION => VALUE ... ] (current usage) +=item cust_svc_unsorted [ OPTION => VALUE ... ] + Returns the services for this package, as FS::cust_svc objects (see L<FS::cust_svc>). Available options are svcpart and svcdb. If either is spcififed, returns only the matching services. +As an optimization, use the cust_svc_unsorted version if you are not displaying +the results. + =cut sub cust_svc { my $self = shift; + cluck "cust_pkg->cust_svc called" if $DEBUG > 2; + $self->_sort_cust_svc( $self->cust_svc_unsorted_arrayref ); +} + +sub cust_svc_unsorted { + my $self = shift; + @{ $self->cust_svc_unsorted_arrayref }; +} + +sub cust_svc_unsorted_arrayref { + my $self = shift; return () unless $self->num_cust_svc(@_); @@ -2645,13 +2661,7 @@ sub cust_svc { $search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} ); } - cluck "cust_pkg->cust_svc called" if $DEBUG > 2; - - #if ( $self->{'_svcnum'} ) { - # values %{ $self->{'_svcnum'}->cache }; - #} else { - $self->_sort_cust_svc( [ qsearch(\%search) ] ); - #} + [ qsearch(\%search) ]; } diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index d6d7d4cf1..958209049 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -113,6 +113,10 @@ my $rt_session; sub delete { my $self = shift; + + my $cust_pkg = $self->cust_pkg; + my $custnum = $cust_pkg->custnum if $cust_pkg; + my $error = $self->SUPER::delete; return $error if $error; @@ -126,7 +130,15 @@ sub delete { $links->Limit(FIELD => 'Target', VALUE => 'freeside://freeside/cust_svc/'.$svcnum); while ( my $l = $links->Next ) { - my ($val, $msg) = $l->Delete; + my ($val, $msg); + if ( $custnum ) { + # re-link to point to the customer instead + ($val, $msg) = + $l->SetTarget('freeside://freeside/cust_main/'.$custnum); + } else { + # unlinked service + ($val, $msg) = $l->Delete; + } # can't do anything useful on error warn "error unlinking ticket $svcnum: $msg\n" if !$val; } diff --git a/FS/FS/invoice_conf.pm b/FS/FS/invoice_conf.pm new file mode 100644 index 000000000..043cab03c --- /dev/null +++ b/FS/FS/invoice_conf.pm @@ -0,0 +1,274 @@ +package FS::invoice_conf; + +use strict; +use base qw( FS::Record FS::Conf ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::invoice_conf - Object methods for invoice_conf records + +=head1 SYNOPSIS + + use FS::invoice_conf; + + $record = new FS::invoice_conf \%hash; + $record = new FS::invoice_conf { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::invoice_conf object represents a set of localized invoice +configuration values. FS::invoice_conf inherits from FS::Record and FS::Conf, +and supports the FS::Conf interface. The following fields are supported: + +=over 4 + +=item confnum - primary key + +=item modenum - L<FS::invoice_mode> foreign key + +=item locale - locale string (see L<FS::Locales>) + +=item notice_name - the title to display on the invoice + +=item subject - subject line of the email + +=item htmlnotes - "notes" section (HTML) + +=item htmlfooter - footer (HTML) + +=item htmlsummary - summary header, for invoices in summary format (HTML) + +=item htmlreturnaddress - return address (HTML) + +=item latexnotes - "notes" section (LaTeX) + +=item latexfooter - footer (LaTeX) + +=item latexsummary - summary header, for invoices in summary format (LaTeX) + +=item latexreturnaddress - return address (LaTeX) + +=item latexcoupon - payment coupon section (LaTeX) + +=item latexsmallfooter - footer for pages after the first (LaTeX) + +=item latextopmargin - top margin + +=item latexheadsep - distance from bottom of header to top of body + +=item latexaddresssep - distance from top of body to customer address + +=item latextextheight - maximum height of invoice body text + +=item latexextracouponspace - additional footer space to allow for coupon + +=item latexcouponfootsep - distance from bottom of coupon content to top +of page footer + +=item latexcouponamountenclosedsep - distance from coupon balance line to +"Amount Enclosed" box + +=item latexcoupontoaddresssep - distance from "Amount Enclosed" box to +coupon mailing address + +=item latexverticalreturnaddress - 'Y' to place the return address below +the company logo rather than beside it + +=item latexcouponaddcompanytoaddress - 'Y' to add the company name to the +address on the payment coupon + +=item logo_png - company logo, as a PNG, for HTML invoices + +=item logo_eps - company logo, as an EPS, for LaTeX invoices + +=item lpr - command to print the invoice (passed on stdin as a PDF) + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new invoice configuration. To add it to the database, see +L<"insert">. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'invoice_conf'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# slightly special: you can insert/replace the invoice mode this way + +sub insert { + my $self = shift; + if (!$self->modenum) { + my $invoice_mode = FS::invoice_mode->new({ + 'modename' => $self->modename, + 'agentnum' => $self->agentnum, + }); + my $error = $invoice_mode->insert; + return $error if $error; + $self->set('modenum' => $invoice_mode->modenum); + } else { + my $invoice_mode = FS::invoice_mode->by_key($self->modenum); + my $changed = 0; + foreach (qw(agentnum modename)) { + $changed ||= ($invoice_mode->get($_) eq $self->get($_)); + $invoice_mode->set($_, $self->get($_)); + } + my $error = $invoice_mode->replace if $changed; + return $error if $error; + } + $self->SUPER::insert(@_); +} + +=item delete + +Delete this record from the database. + +=cut + +sub delete { + my $self = shift; + my $error = $self->FS::Record::delete; # not Conf::delete + return $error if $error; + my $invoice_mode = FS::invoice_mode->by_key($self->modenum); + if ( $invoice_mode and + FS::invoice_conf->count('modenum = '.$invoice_mode->modenum) == 0 ) { + $error = $invoice_mode->delete; + return $error if $error; + } + ''; +} + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +sub replace { + my $self = shift; + my $error = $self->SUPER::replace(@_); + return $error if $error; + + my $invoice_mode = FS::invoice_mode->by_key($self->modenum); + my $changed = 0; + foreach (qw(agentnum modename)) { + $changed ||= ($invoice_mode->get($_) eq $self->get($_)); + $invoice_mode->set($_, $self->get($_)); + } + $error = $invoice_mode->replace if $changed; + return $error if $error; +} + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('confnum') + || $self->ut_number('modenum') + || $self->ut_textn('locale') + || $self->ut_anything('notice_name') + || $self->ut_anything('subject') + || $self->ut_anything('htmlnotes') + || $self->ut_anything('htmlfooter') + || $self->ut_anything('htmlsummary') + || $self->ut_anything('htmlreturnaddress') + || $self->ut_anything('latexnotes') + || $self->ut_anything('latexfooter') + || $self->ut_anything('latexsummary') + || $self->ut_anything('latexcoupon') + || $self->ut_anything('latexsmallfooter') + || $self->ut_anything('latexreturnaddress') + || $self->ut_textn('latextopmargin') + || $self->ut_textn('latexheadsep') + || $self->ut_textn('latexaddresssep') + || $self->ut_textn('latextextheight') + || $self->ut_textn('latexextracouponspace') + || $self->ut_textn('latexcouponfootsep') + || $self->ut_textn('latexcouponamountenclosedsep') + || $self->ut_textn('latexcoupontoaddresssep') + || $self->ut_flag('latexverticalreturnaddress') + || $self->ut_flag('latexcouponaddcompanytoaddress') + || $self->ut_anything('logo_png') + || $self->ut_anything('logo_eps') + ; + return $error if $error; + + $self->SUPER::check; +} + +# hook _config to substitute our own values; let FS::Conf do the rest of +# the interface + +sub _config { + my $self = shift; + # if we fall back, we still want FS::Conf to respect our locale + $self->{locale} = $self->get('locale'); + my ($key, $agentnum, $nodefault) = @_; + # some fields, but not all, start with invoice_ + my $colname = $key; + if ( $key =~ /^invoice_(.*)$/ ) { + $colname = $1; + } + if ( length($self->get($colname)) ) { + return FS::conf->new({ 'name' => $key, + 'value' => $self->get($colname) }); + } else { + return $self->FS::Conf::_config(@_); + } +} + +# disambiguation +sub set { + my $self = shift; + $self->FS::Record::set(@_); +} + +sub exists { + my $self = shift; + $self->FS::Conf::exists(@_); +} + +=back + +=head1 SEE ALSO + +L<FS::Template_Mixin>, L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/invoice_mode.pm b/FS/FS/invoice_mode.pm new file mode 100644 index 000000000..115dd4469 --- /dev/null +++ b/FS/FS/invoice_mode.pm @@ -0,0 +1,157 @@ +package FS::invoice_mode; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use FS::invoice_conf; + +=head1 NAME + +FS::invoice_mode - Object methods for invoice_mode records + +=head1 SYNOPSIS + + use FS::invoice_mode; + + $record = new FS::invoice_mode \%hash; + $record = new FS::invoice_mode { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::invoice_mode object represents an invoice rendering style. +FS::invoice_mode inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item modenum - primary key + +=item agentnum - the agent who owns this invoice mode (can be null) + +=item modename - descriptive name for internal use + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new invoice mode. To add the object to the database, +see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'invoice_mode'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('modenum') + || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum') + || $self->ut_text('modename') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item invoice_conf [ LOCALE ] + +Returns the L<FS::invoice_conf> for this invoice mode, with the specified +locale. If there isn't one with that locale, returns the one with null +locale. If that doesn't exist, returns nothing. + +=cut + +sub invoice_conf { + my $self = shift; + my $locale = shift; + my $invoice_conf; + if ( $locale ) { + $invoice_conf = qsearchs('invoice_conf', { + modenum => $self->modenum, + locale => $locale, + }); + } + $invoice_conf ||= qsearchs('invoice_conf', { + modenum => $self->modenum, + locale => '', + }); + $invoice_conf; +} + +=item agent + +Returns the agent associated with this invoice mode, if any. + +=cut + +sub agent { + my $self = shift; + $self->agentnum ? FS::agent->by_key($self->agentnum) : ''; +} + +=back + +=head1 SEE ALSO + +L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_event/Action/cust_bill_email.pm b/FS/FS/part_event/Action/cust_bill_email.pm index 1a3bca4b7..3331a4cb6 100644 --- a/FS/FS/part_event/Action/cust_bill_email.pm +++ b/FS/FS/part_event/Action/cust_bill_email.pm @@ -9,14 +9,22 @@ sub eventtable_hashref { { 'cust_bill' => 1 }; } +sub option_fields { + ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, + ); +} + sub default_weight { 51; } sub do_action { my( $self, $cust_bill ) = @_; - #my $cust_main = $self->cust_main($cust_bill); my $cust_main = $cust_bill->cust_main; + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->email unless $cust_main->invoice_noemail; } diff --git a/FS/FS/part_event/Action/cust_bill_print.pm b/FS/FS/part_event/Action/cust_bill_print.pm index 6b3e6f460..ea6e0aa8e 100644 --- a/FS/FS/part_event/Action/cust_bill_print.pm +++ b/FS/FS/part_event/Action/cust_bill_print.pm @@ -9,6 +9,14 @@ sub eventtable_hashref { { 'cust_bill' => 1 }; } +sub option_fields { + ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, + ); +} + sub default_weight { 51; } sub do_action { @@ -17,6 +25,7 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); my $cust_main = $cust_bill->cust_main; + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->print; } diff --git a/FS/FS/part_event/Action/cust_bill_print_pdf.pm b/FS/FS/part_event/Action/cust_bill_print_pdf.pm index 6b37f389f..6c01d4294 100644 --- a/FS/FS/part_event/Action/cust_bill_print_pdf.pm +++ b/FS/FS/part_event/Action/cust_bill_print_pdf.pm @@ -9,6 +9,14 @@ sub eventtable_hashref { { 'cust_bill' => 1 }; } +sub option_fields { + ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode' + }, + ); +} + sub default_weight { 51; } sub do_action { @@ -20,6 +28,7 @@ sub do_action { my $opt = { $self->options }; $opt->{'notice_name'} ||= 'Invoice'; + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->batch_invoice($opt); } diff --git a/FS/FS/part_event/Action/cust_bill_send.pm b/FS/FS/part_event/Action/cust_bill_send.pm index 587a7c664..c6928dc00 100644 --- a/FS/FS/part_event/Action/cust_bill_send.pm +++ b/FS/FS/part_event/Action/cust_bill_send.pm @@ -9,11 +9,20 @@ sub eventtable_hashref { { 'cust_bill' => 1 }; } +sub option_fields { + ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, + ); +} + sub default_weight { 50; } sub do_action { my( $self, $cust_bill ) = @_; + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->send; } diff --git a/FS/FS/part_event/Action/cust_bill_send_agent.pm b/FS/FS/part_event/Action/cust_bill_send_agent.pm index 670a32c5b..bbb757b59 100644 --- a/FS/FS/part_event/Action/cust_bill_send_agent.pm +++ b/FS/FS/part_event/Action/cust_bill_send_agent.pm @@ -7,6 +7,9 @@ sub description { 'Send invoice (email/print/fax) with alternate template, for specific agents'; } +# this event is just cust_bill_send_alternate + an implicit (and inefficient) +# 'agent' condition + sub eventtable_hashref { { 'cust_bill' => 1 }; } @@ -17,6 +20,9 @@ sub option_fields { type => 'select-agent', multiple => 1 }, + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, 'agent_templatename' => { label => 'Template', type => 'select-invoice_template', }, @@ -32,10 +38,15 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); my $cust_main = $cust_bill->cust_main; + my %agentnums = map { $_=>1 } split(/\s*,\s*/, $self->option('agentnum')); + if (keys(%agentnums) and !exists($agentnums{$cust_main->agentnum})) { + return; + } + + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->send( - $self->option('agent_templatename'), - [ split(/\s*,\s*/, $self->option('agentnum') ) ], - $self->option('agent_invoice_from'), + 'template' => $self->option('agent_templatename'), + 'invoice_from' => $self->option('agent_invoice_from'), ); } diff --git a/FS/FS/part_event/Action/cust_bill_send_alternate.pm b/FS/FS/part_event/Action/cust_bill_send_alternate.pm index cfd9264d8..fb71a5a39 100644 --- a/FS/FS/part_event/Action/cust_bill_send_alternate.pm +++ b/FS/FS/part_event/Action/cust_bill_send_alternate.pm @@ -11,6 +11,8 @@ sub eventtable_hashref { sub option_fields { ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode' }, 'templatename' => { label => 'Template', type => 'select-invoice_template', }, @@ -25,7 +27,8 @@ sub do_action { #my $cust_main = $self->cust_main($cust_bill); my $cust_main = $cust_bill->cust_main; - $cust_bill->send( $self->option('templatename') ); + $cust_bill->set('mode' => $self->option('modenum')); + $cust_bill->send({'template' => $self->option('templatename')}); } 1; diff --git a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm index 083da8b08..c744362ce 100644 --- a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm +++ b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm @@ -18,6 +18,9 @@ sub eventtable_hashref { sub option_fields { ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, 'if_newest_templatename' => { label => 'Template', type => 'select-invoice_template', }, @@ -29,10 +32,17 @@ sub default_weight { 50; } sub do_action { my( $self, $cust_bill ) = @_; - #my $cust_main = $self->cust_main($cust_bill); - my $cust_main = $cust_bill->cust_main; - - $cust_bill->send( $self->option('templatename') ); + my $invnum = $cust_bill->invnum; + my $custnum = $cust_bill->custnum; + return '' if scalar( + grep { $_->owed > 0 } + qsearch('cust_bill', { + 'custnum' => $custnum, + 'invnum' => { op=>'>', value=>$invnum }, + }) + ); + $cust_bill->set('mode' => $self->option('modenum')); + $cust_bill->send( 'template' => $self->option('templatename') ); } 1; diff --git a/FS/FS/part_event/Action/cust_bill_send_reminder.pm b/FS/FS/part_event/Action/cust_bill_send_reminder.pm index 073bb8fd3..354f969d4 100644 --- a/FS/FS/part_event/Action/cust_bill_send_reminder.pm +++ b/FS/FS/part_event/Action/cust_bill_send_reminder.pm @@ -11,9 +11,13 @@ sub eventtable_hashref { sub option_fields { ( + 'modenum' => { label => 'Invoice mode', + type => 'select-invoice_mode', + }, + # totally unnecessary, since the invoice mode can set notice_name and lpr, + # but for compatibility... 'notice_name' => 'Reminder name', - #'notes' => { 'label' => 'Reminder notes' }, - #include standard notes? no/prepend/append + #'notes' => { 'label' => 'Reminder notes' }, # invoice mode does this 'lpr' => 'Optional alternate print command', ); } @@ -23,9 +27,7 @@ sub default_weight { 50; } sub do_action { my( $self, $cust_bill ) = @_; - #my $cust_main = $self->cust_main($cust_bill); - #my $cust_main = $cust_bill->cust_main; - + $cust_bill->set('mode' => $self->option('modenum')); $cust_bill->send({ 'notice_name' => $self->option('notice_name'), 'lpr' => $self->option('lpr'), diff --git a/FS/FS/part_event/Action/cust_statement_send.pm b/FS/FS/part_event/Action/cust_statement_send.pm index 74cc48ca8..67a94aaa1 100644 --- a/FS/FS/part_event/Action/cust_statement_send.pm +++ b/FS/FS/part_event/Action/cust_statement_send.pm @@ -19,7 +19,8 @@ sub default_weight { sub do_action { my( $self, $cust_statement ) = @_; - $cust_statement->send( 'statement' ); #XXX configure + $cust_statement->send( 'template' => 'statement' ); #XXX configure + #XXX use an invoice mode? } diff --git a/FS/FS/part_event/Action/fee.pm b/FS/FS/part_event/Action/fee.pm index cd9e200c8..c2b4673fa 100644 --- a/FS/FS/part_event/Action/fee.pm +++ b/FS/FS/part_event/Action/fee.pm @@ -32,7 +32,48 @@ sub _calc_fee { if ( $balance >= 0 ) { return 0; } elsif ( (-1 * $balance) < $self->option('charge') ) { - return -1 * $balance; + my $total = -1 * $balance; + # if it's tax exempt, then we're done + # XXX we also bail out if you're using external tax tables, because + # they're definitely NOT linear and we haven't yet had a reason to + # make that case work. + return $total if $self->option('setuptax') eq 'Y' + or FS::Conf->new->exists('enable_taxproducts'); + + # estimate tax rate + # false laziness with xmlhttp-calculate_taxes, cust_main::Billing, etc. + # XXX not accurate with monthly exemptions + my $cust_main = $cust_object->cust_main; + my $taxlisthash = {}; + my $charge = FS::cust_bill_pkg->new({ + setup => $total, + recur => 0, + details => [] + }); + my $part_pkg = FS::part_pkg->new({ + taxclass => $self->option('taxclass') + }); + my $error = $cust_main->_handle_taxes( + FS::part_pkg->new({ taxclass => ($self->option('taxclass') || '') }), + $taxlisthash, + $charge, + FS::cust_pkg->new({custnum => $cust_main->custnum}), + ); + if ( $error ) { + warn "error estimating taxes for breakage charge: custnum ".$cust_main->custnum."\n"; + return $total; + } + # $taxlisthash: tax identifier => [ cust_main_county, cust_bill_pkg... ] + my $total_rate = 0; + my @taxes = map { $_->[0] } values %$taxlisthash; + foreach (@taxes) { + $total_rate += $_->tax; + } + return $total if $total_rate == 0; # no taxes apply + + my $total_cents = $total * 100; + my $charge_cents = sprintf('%.0f', $total_cents * 100/(100 + $total_rate)); + return ($charge_cents / 100); } } diff --git a/FS/FS/part_event/Condition/pkg_age_Common.pm b/FS/FS/part_event/Condition/pkg_age_Common.pm index 726b01d70..33e49b8a6 100644 --- a/FS/FS/part_event/Condition/pkg_age_Common.pm +++ b/FS/FS/part_event/Condition/pkg_age_Common.pm @@ -49,7 +49,7 @@ sub condition { } sub pkg_age_age { - my( $self, $cust_pkg, %opt ); + my( $self, $cust_pkg, %opt ) = @_; $self->option_age_from('age', $opt{'time'} ); } diff --git a/FS/FS/part_export/domain_shellcommands.pm b/FS/FS/part_export/domain_shellcommands.pm index 582e29217..8e85d71e1 100644 --- a/FS/FS/part_export/domain_shellcommands.pm +++ b/FS/FS/part_export/domain_shellcommands.pm @@ -49,8 +49,7 @@ The following variables are available for interpolation (prefixed with <code>new <LI><code>$uid</code> - of catchall account <LI><code>$gid</code> - of catchall account <LI><code>$dir</code> - home directory of catchall account - <LI>All other fields in - <a href="../docs/schema.html#svc_domain">svc_domain</a> are also available. + <LI>All other fields in <b>svc_domain</b> are also available. </UL> END ); diff --git a/FS/FS/part_export/shellcommands_withdomain.pm b/FS/FS/part_export/shellcommands_withdomain.pm index 1b59589bf..29715b75b 100644 --- a/FS/FS/part_export/shellcommands_withdomain.pm +++ b/FS/FS/part_export/shellcommands_withdomain.pm @@ -141,7 +141,32 @@ The following variables are available for interpolation (prefixed with <LI><code>$shell</code> <LI><code>$quota</code> <LI><code>@radius_groups</code> - <LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available. + <LI><code>$reasonnum (when suspending)</code> + <LI><code>$reasontext (when suspending)</code> + <LI><code>$reasontypenum (when suspending)</code> + <LI><code>$reasontypetext (when suspending)</code> + <LI><code>$pkgnum</code> + <LI><code>$custnum</code> + <LI>All other fields in <b>svc_acct</b> are also available. + <LI>The following fields from <b>cust_main</b> are also available (except during replace): company, address1, address2, city, state, zip, county, daytime, night, fax, otaker, agent_custid, locale. When used on the command line (rather than STDIN), they will be quoted for the shell already (do not add additional quotes). +</UL> +For the package changed command only, the following fields are also available: +<UL> + <LI>$old_pkgnum and $new_pkgnum + <LI>$old_pkgpart and $new_pkgpart + <LI>$old_agent_pkgid and $new_agent_pkgid + <LI>$old_order_date and $new_order_date + <LI>$old_start_date and $new_start_date + <LI>$old_setup and $new_setup + <LI>$old_bill and $new_bill + <LI>$old_last_bill and $new_last_bill + <LI>$old_susp and $new_susp + <LI>$old_adjourn and $new_adjourn + <LI>$old_resume and $new_resume + <LI>$old_cancel and $new_cancel + <LI>$old_unancel and $new_unancel + <LI>$old_expire and $new_expire + <LI>$old_contract_end and $new_contract_end </UL> END ); diff --git a/FS/FS/svc_cable.pm b/FS/FS/svc_cable.pm index 1980c0ee9..596f69995 100644 --- a/FS/FS/svc_cable.pm +++ b/FS/FS/svc_cable.pm @@ -4,6 +4,7 @@ use base qw( FS::svc_Common ); #qw( FS::device_Common FS::svc_Common ); use strict; use Tie::IxHash; use FS::Record qw( qsearchs ); # qw( qsearch qsearchs ); +use FS::cable_provider; use FS::cable_model; =head1 NAME @@ -72,24 +73,35 @@ sub search_sql { sub table_info { tie my %fields, 'Tie::IxHash', - 'svcnum' => 'Service', - 'modelnum' => { label => 'Model', - type => 'select-cable_model', - disable_inventory => 1, - disable_select => 1, - value_callback => sub { - my $svc = shift; - $svc->cable_model->model_name; - }, - }, - 'serialnum' => 'Serial number', - 'mac_addr' => { label => 'MAC address', - type => 'input-mac_addr', - value_callback => sub { - my $svc = shift; - join(':', $svc->mac_addr =~ /../g); - }, - }, + 'svcnum' => 'Service', + 'providernum' => { label => 'Provider', + type => 'select-cable_provider', + disable_inventory => 1, + disable_select => 1, + value_callback => sub { + my $svc = shift; + my $p = $svc->cable_provider; + $p ? $p->provider : ''; + }, + }, + #XXX "Circuit ID/Order number" + 'modelnum' => { label => 'Model', + type => 'select-cable_model', + disable_inventory => 1, + disable_select => 1, + value_callback => sub { + my $svc = shift; + $svc->cable_model->model_name; + }, + }, + 'serialnum' => 'Serial number', + 'mac_addr' => { label => 'MAC address', + type => 'input-mac_addr', + value_callback => sub { + my $svc = shift; + join(':', $svc->mac_addr =~ /../g); + }, + }, ; { @@ -130,6 +142,7 @@ sub check { my $error = $self->ut_numbern('svcnum') + || $self->ut_foreign_key('providernum', 'cable_provider', 'providernum') || $self->ut_foreign_key('modelnum', 'cable_model', 'modelnum') || $self->ut_alpha('serialnum') || $self->ut_mac_addr('mac_addr') @@ -139,6 +152,17 @@ sub check { $self->SUPER::check; } +=item cable_provider + +Returns the cable_provider object for this record. + +=cut + +sub cable_provider { + my $self = shift; + qsearchs('cable_provider', { 'providernum'=>$self->providernum } ); +} + =item cable_model Returns the cable_model object for this record. diff --git a/FS/MANIFEST b/FS/MANIFEST index a3b11f717..5dbe754c1 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -720,3 +720,9 @@ FS/svc_alarm.pm t/svc_alarm.t FS/cable_model.pm t/cable_model.t +FS/invoice_mode.pm +t/invoice_mode.t +FS/invoice_conf.pm +t/invoice_conf.t +FS/cable_provider.pm +t/cable_provider.t diff --git a/FS/t/cable_provider.t b/FS/t/cable_provider.t new file mode 100644 index 000000000..c794379a9 --- /dev/null +++ b/FS/t/cable_provider.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cable_provider; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/invoice_conf.t b/FS/t/invoice_conf.t new file mode 100644 index 000000000..b707fa3f0 --- /dev/null +++ b/FS/t/invoice_conf.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::invoice_conf; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/invoice_mode.t b/FS/t/invoice_mode.t new file mode 100644 index 000000000..5f945f0d4 --- /dev/null +++ b/FS/t/invoice_mode.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::invoice_mode; +$loaded=1; +print "ok 1\n"; diff --git a/bin/generate-table-module b/bin/generate-table-module index b536360c5..37a581200 100755 --- a/bin/generate-table-module +++ b/bin/generate-table-module @@ -95,10 +95,10 @@ close TEST; # add them to MANIFEST ### -#system('cvs edit FS/MANIFEST'); - open(MANIFEST,">>FS/MANIFEST") or die $!; print MANIFEST "FS/$table.pm\n", "t/$table.t\n"; close MANIFEST; +system("git add FS/FS/$table.pm FS/t/$table.t"); + diff --git a/bin/standardize-locations b/bin/standardize-locations new file mode 100755 index 000000000..6e5fd3c16 --- /dev/null +++ b/bin/standardize-locations @@ -0,0 +1,25 @@ +#!/usr/bin/perl -w + +use strict; +use FS::UID 'adminsuidsetup'; +use FS::Conf; +use FS::queue; + +my $user = shift or die "usage:\n standardize-locations user"; +adminsuidsetup($user); +my $conf = FS::Conf->new; +my $method = $conf->config('address_standardize_method') + or die "No address standardization method configured.\n"; +if ($method eq 'usps') { + # we're not supposed to do this + # (allow it anyway with a warning?) + die "USPS standardization does not allow batch processing.\n"; +} +my $job = FS::queue->new({ + job => 'FS::cust_location::process_standardize' +}); +my $error = $job->insert('_JOB'); +die $error if $error; +print "Address standardization job scheduled.\n"; + +1; diff --git a/bin/test-event b/bin/test-event index d3a9f110d..73c9d31ec 100644..100755 --- a/bin/test-event +++ b/bin/test-event @@ -34,7 +34,10 @@ print "\n"; my @conditions = $part_event->part_event_condition; foreach my $condition ( @conditions ) { - my $sat = $condition->condition( $object, 'cust_event' => $cust_event ); + my $sat = $condition->condition( $object, + 'cust_event' => $cust_event, + 'time' => time, + ); my $sql = $condition->condition_sql(); diff --git a/httemplate/browse/cable_provider.html b/httemplate/browse/cable_provider.html new file mode 100644 index 000000000..0d344984b --- /dev/null +++ b/httemplate/browse/cable_provider.html @@ -0,0 +1,32 @@ +<& elements/browse.html, + 'title' => 'Cable providers', + 'html_init' => $html_init, + 'name' => 'providers', + 'disableable' => 1, + 'disabled_statuspos' => 1, + 'query' => { 'table' => 'cable_provider', + 'hashref' => {}, + 'order_by' => 'ORDER BY provider', + }, + 'count_query' => $count_query, + 'header' => $header, + 'fields' => $fields, + 'links' => $links, +&> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $html_init = + qq!<A HREF="${p}edit/cable_provider.html"><I>Add a provider</I></A><BR><BR>!; + +my $count_query = 'SELECT COUNT(*) FROM cable_provider'; + +my $link = [ $p.'edit/cable_provider.html?', 'providernum' ]; + +my $header = [ 'Provider' ]; +my $fields = [ 'provider' ]; +my $links = [ $link ]; + +</%init> diff --git a/httemplate/browse/invoice_conf.html b/httemplate/browse/invoice_conf.html new file mode 100644 index 000000000..c8fd1bffb --- /dev/null +++ b/httemplate/browse/invoice_conf.html @@ -0,0 +1,70 @@ +<& elements/browse.html, + 'title' => 'Invoice modes', + 'name_singular' => 'configuration', + 'menubar' => \@menubar, + 'query' => { + 'select' => $select, + 'table' => 'invoice_conf', + 'addl_from' => ' JOIN invoice_mode USING (modenum)', + 'extra_sql' => ' WHERE '.$curuser->agentnums_sql( + 'table' => 'invoice_mode', + 'null_right' => ['Edit global templates'], + ), + 'order_by' => q( ORDER BY modename asc, COALESCE(locale,'') asc), + }, + 'count_query' => 'SELECT COUNT(*) FROM invoice_conf JOIN invoice_mode USING (modenum)', + 'header' => [ 'Name', 'Agent', 'Locale', 'Overrides', ], + 'fields' => [ $modename, + $agent, + $locale_label, + $overrides, + ], + 'align' => 'llcl', + 'links' => [ '', '', $link ], + 'disable_maxselect' => 1, +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +die "access denied" + unless $curuser->access_right([ 'View templates', 'View global templates', + 'Edit templates', 'Edit global templates', ]); + +my @overrides = grep {$_ ne 'modenum' and $_ ne 'confnum'} FS::invoice_conf->fields; +my $select = join(',', 'modename', 'agentnum', 'confnum', 'invoice_conf.*'); + +my @menubar = (); +if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) { + push @menubar, 'Add a new invoice mode' => $p.'edit/invoice_conf.html'; +} + +my $locale_style = 'font-size:0.8em; padding:3px; background-color:'; + +my $last_modenum = 0; +my $modename = sub { + return '' if $_[0]->modenum == $last_modenum; + $_[0]->modename; +}; + +my $agent = sub { + return '' if $_[0]->modenum == $last_modenum; + $last_modenum = $_[0]->modenum; + $_[0]->agentnum ? FS::agent->by_key($_[0]->agentnum)->agent : '(global)'; +}; + +my $locale_label = sub { + my $l = $_[0]->locale; + $l ? +{ FS::Locales->locale_info($l) }->{'label'} : '(default)'; +}; + +my $overrides = sub { + my $invoice_conf = shift; + [ map { [ { data => $_ } ] } + grep { length $invoice_conf->get($_) } + @overrides + ], +}; + +my $link = [ $p.'edit/invoice_conf.html?', 'confnum' ]; +</%init> diff --git a/httemplate/docs/about.html b/httemplate/docs/about.html index c2ba4e4a0..80d9488b6 100644 --- a/httemplate/docs/about.html +++ b/httemplate/docs/about.html @@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR> % unless ( $agentnum ) { <CENTER> - <FONT SIZE="-3">"I can't figure out ... if it's an end or the beginning" - R. Hunter</FONT> + <FONT SIZE="-3">"" - R. Hunter</FONT> </CENTER> % } diff --git a/httemplate/edit/cable_provider.html b/httemplate/edit/cable_provider.html new file mode 100644 index 000000000..9a911ccfa --- /dev/null +++ b/httemplate/edit/cable_provider.html @@ -0,0 +1,20 @@ +<& elements/edit.html, + 'name_singular' => 'Provider', + 'table' => 'cable_provider', + 'fields' => [ + 'provider', + { field=>'disabled', type=>'checkbox', value=>'Y', }, + ], + 'labels' => { + 'providernum' => 'Provider', + 'provider' => 'Provider', + 'disabled' => 'Disabled', + }, + 'viewall_dir' => 'browse', +&> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js index 9e18fa0df..ecfcb3cbc 100644 --- a/httemplate/edit/cust_main/bottomfixup.js +++ b/httemplate/edit/cust_main/bottomfixup.js @@ -10,17 +10,20 @@ my @fixups = ('copy_payby_fields', 'standardize_locations'); push @fixups, 'confirm_censustract' if $conf->exists('cust_main-require_censustract'); -# currently doesn't work; disable to avoid problems -#push @fixups, 'check_unique' -# if $conf->exists('cust_main-check_unique') and !$opt{'custnum'}; +my $uniqueness = $conf->config('cust_main-check_unique'); +push @fixups, 'check_unique' + if $uniqueness and !$opt{'custnum'}; push @fixups, 'do_submit'; # always last </%init> - var fixups = <% encode_json(\@fixups) %>; var fixup_position; var running = false; +<&| /elements/onload.js &> +submit_abort(); +</&> + %# state machine to deal with all the asynchronous stuff we're doing %# call this after each fixup on success: function submit_continue() { @@ -132,10 +135,14 @@ function set_censustract(tract, year) { } function check_unique() { - var search_hash = new Object; -% foreach ($conf->config('cust_main-check_unique')) { - search_hash['<% $_ %>'] = document.CustomerForm.elements['<% $_ %>'].value; + var search_hash = {}; +% if ($uniqueness eq 'address') { + search_hash['address'] = [ + document.CustomerForm.elements['bill_address1'].value, + document.CustomerForm.elements['ship_address1'].value + ]; % } +%# no other options yet %# supported in IE8+, Firefox 3.5+, WebKit, Opera 10.5+ duplicates_form(JSON.stringify(search_hash), confirm_unique); diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html index ebd9b927c..e25506f52 100644 --- a/httemplate/edit/cust_main/top_misc.html +++ b/httemplate/edit/cust_main/top_misc.html @@ -154,9 +154,10 @@ % } else { - <& /elements/tr-select-part_referral.html, - 'curr_value' => $refnum - &> + <& /elements/tr-select-part_referral.html, + 'curr_value' => $refnum, + 'label' => "<B>${r}".emt('Advertising source')."</B>" + &> % } diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index 060281115..6c965326b 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -338,6 +338,7 @@ Example: % qw( width height config ), #htmlarea % qw( alt_format ), #select-cust_location % qw( classnum ), # select-inventory_item +% qw( aligned ), # columnstart % ; % % #select-table diff --git a/httemplate/edit/invoice_conf.html b/httemplate/edit/invoice_conf.html new file mode 100644 index 000000000..b7b3a4ebc --- /dev/null +++ b/httemplate/edit/invoice_conf.html @@ -0,0 +1,296 @@ +<& elements/edit.html, + 'body_etc' => $body_etc, + 'name_singular' => 'invoice configuration', + 'table' => 'invoice_conf', + 'viewall_dir' => 'browse', + 'fields' => \@fields, + 'labels' => \%labels, + 'new_callback' => \&new_callback, + 'edit_callback' => \&edit_callback, + 'error_callback' => \&error_callback, + 'html_init' => \&html_init, + 'html_table_bottom' => \&html_table_bottom, + 'html_bottom' => '</DIV>', # close tablebreak-tabs +&> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; + +# ??? +die "access denied" + unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]); + +my $body_etc = ''; +$body_etc = q!onload="document.getElementById('locale').onchange()"! + if $cgi->param('locale') eq 'new'; + +my $modenum = $cgi->param('modenum'); +my $mode = $modenum ? qsearchs('invoice_mode', { modenum => $modenum }) : ''; + +my %textarea = (type => 'textarea', rows => 10, cols => 40); +my @fields = ( + { field => 'modenum', type => 'hidden' }, + { field => 'agentnum', + type => 'select-agent', + }, + { field => 'modename', size=>60, }, + { type => 'tablebreak-tabs', + include_opt_callback => \&menubar_opt_callback, + }, + { field => 'locale', type => 'hidden' }, + { field => 'notice_name', size=>60, }, + { field => 'subject', size=>60, }, + { field => 'lpr', size=>60, }, + + { type => 'columnstart', aligned => 1 }, + { type => 'title', value => '<BR>' }, + map ( { +{ type => 'justtitle', value => $_ } } + 'Notes', + 'Footer', + 'Summary header', + 'Return address', + 'Coupon', + 'Small footer', + 'Top margin', + 'Header separation', + 'Address separation', + 'Text height', + 'Coupon height', + 'Footer separation', + ), + + { type => 'columnnext' }, + { type => 'title', value => 'LaTeX' }, + { field => 'latexnotes', %textarea }, + { field => 'latexfooter', %textarea }, + { field => 'latexsummary', %textarea }, + { field => 'latexreturnaddress', %textarea }, + { field => 'latexcoupon', %textarea }, + { field => 'latexsmallfooter', %textarea }, + { field => 'latextopmargin', size => 16 }, + { field => 'latexheadsep', size => 16 }, + { field => 'latexaddresssep', size => 16 }, + { field => 'latextextheight', size => 16 }, + { field => 'latexextracouponspace', size => 16 }, + { field => 'latexcouponfootsep', size => 16 }, + # are these still used? + #{ field => 'latexcouponamountenclosedsep', size => 16 }, + #{ field => 'latexverticalreturnaddress', type => 'checkbox' }, + #{ field => 'latexcouponaddcompanytoaddress',type => 'checkbox' }, + # logo -- implement if someone really needs it... + + { type => 'columnnext' }, + { type => 'title', value => 'HTML' }, + { field => 'htmlnotes', %textarea }, #htmlarea? + { field => 'htmlfooter', %textarea }, + { field => 'htmlsummary', %textarea }, + { field => 'htmlreturnaddress', %textarea }, + # logo + + { type => 'columnend' }, +); + +my %labels = ( + 'confnum' => 'Configuration', + 'locale' => 'Locale', + 'agentnum' => 'Agent', + 'modename' => 'Mode name', + 'notice_name' => 'Notice name', + 'subject' => 'Email Subject: header', + 'lpr' => 'Alternate lpr command', + + map { $_ => '' } (qw( + latexnotes + latexfooter + latexsummary + latexreturnaddress + latexcoupon + latexsmallfooter + latextopmargin + latexheadsep + latexaddresssep + latextextheight + latexextracouponspace + latexcouponfootsep + htmlnotes + htmlfooter + htmlsummary + htmlreturnaddress + logo_png + logo_eps + ) ), + +); + +sub get_invoice_mode { # because we can't quite use agent_virt here + my $modenum = shift; + qsearchs({ + 'table' => 'invoice_mode', + 'hashref' => { 'modenum' => $modenum }, + 'extra_sql' => ' AND '. + $FS::CurrentUser::CurrentUser->agentnums_sql( + 'null_right' => 'Edit global templates', + 'viewall_right' => 'Edit global templates' ), + }); +}; + +sub error_callback { + my ($cgi, $object) = @_; + foreach (qw(modename agentnum)) { + $object->set($_, $cgi->param($_)); + } + if ($object->confnum) { + return edit_callback(@_); + } else { + return new_callback(@_); + } +} + +sub new_callback { + my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_; + my $modenum; + if ( $cgi->param('locale') =~ /^(\w+)$/ ) { + $object->set('locale' => $1); + } + + if ( $cgi->param('modenum') =~ /^(\d+)$/ ) { + $modenum = $1; # we're adding a locale to an existing mode + $object->set('modenum' => $modenum); + my $invoice_mode = get_invoice_mode($modenum) + or die "invoice mode $modenum not found"; + + $object->set('modename', $invoice_mode->modename); + $object->set('agentnum', $invoice_mode->agentnum); + + # also, need to select a locale + # make a list of available locales + my %existing_locales = map { $_->locale } + qsearch('invoice_conf', { modenum => $modenum }); + + my @locales = grep { !exists($existing_locales{$_}) } + FS::Conf->new->config('available-locales'); + my %labels; + foreach (@locales) { + my %info = FS::Locales->locale_info($_); + $labels{$_} = $info{'label'}; + } + unshift @locales, 'new'; + $labels{'new'} = 'Select language'; + + # insert a field def + my $i = 0; + $i++ until ( $fields_arrayref->[$i]->{'field'} eq 'locale' ); + my $locale_field = $fields_arrayref->[$i]; + + my $onchange_locale = "document.getElementById('submit').disabled = + (this.options[this.selectedIndex].value == 'new');"; + + %$locale_field = ( + field => 'locale', + type => 'select', + options => \@locales, + labels => \%labels, + curr_value => 'new', + onchange => $onchange_locale, + ); + + } # otherwise it's a completely new mode, so the locale is default + +} + +sub edit_callback { + # massive false laziness with msg_template UI + my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_; + + # a little different here in that we treat the content object + # as "primary" (this is edit/invoice_conf.html, etc.) + # so all we need from the invoice_mode is its name + # (and agent identity) + my $modenum = $object->modenum; + my $invoice_mode = get_invoice_mode($modenum) + or die "invoice mode $modenum not found"; + $object->set('modename', $invoice_mode->modename); + $object->set('agentnum', $invoice_mode->agentnum); + +} + +sub menubar_opt_callback { + my $object = shift; + my $modenum = $object->modenum or return; + my (@tabs, @options, %labels); + my $display_new = 0; + my $selected = ''; + foreach my $l ('', FS::Conf->new->config('available-locales')) { + my $invoice_conf = + qsearchs('invoice_conf', { modenum => $modenum, locale => $l }); + if ( $invoice_conf ) { + my %info = FS::Locales->locale_info($l) if $l; + my $label = $info{'label'} || mt('Default'); + push @tabs, $label, $invoice_conf->confnum; + $selected = $label if $object->locale eq $l; + } + else { + $display_new = 1; # there is at least one unused locale left + } + } + push @tabs, mt('New'), "modenum=$modenum;locale=new" if $display_new; + $selected = mt('New') if $object->locale eq 'new'; + $selected ||= mt('Default'); + ( + 'url_base' => $cgi->url() . '?', + 'selected' => $selected, + 'tabs' => \@tabs + ); +} + +sub html_init { +q! +<STYLE> +.fstabcontainer th { vertical-align: middle; text-align: center } +</STYLE> +! +} + +sub html_table_bottom { + my $object = shift; + my $locale = ''; + my $modenum = ''; + + if ($object->locale =~ /^(\w+)$/) { + $locale = $1; + } + if ($object->modenum =~ /^(\d+)$/) { + $modenum = $1; + } + my $html; + my $show_delete = 1; + # don't allow the default locale to be removed unless it's the last one + # in the mode + $show_delete = 0 if ( + $locale eq 'new' or + $modenum eq '' or + ($locale eq '' and + FS::invoice_conf->count("modenum = $modenum and locale is not null") > 0 + ) + ); + + if ( $show_delete ) { + # set up a delete link + my $confnum = $object->confnum; + my $url = $p."misc/delete-invoice_conf.html?$confnum"; + my $link = qq!<A HREF="javascript:areyousure('$url','Really delete this configuration?')">! . + 'Delete this configuration' . + '</A>'; + $html = qq!<TR><TD></TD> + <TD STYLE="font-style: italic; font-size: small">$link</TD></TR> + <SCRIPT TYPE="text/javascript"> + function areyousure(url, message) { + if (confirm(message)) window.location.href = url; + } + </SCRIPT> + !; + } + $html; +} + +</%init> diff --git a/httemplate/edit/process/cable_provider.html b/httemplate/edit/process/cable_provider.html new file mode 100644 index 000000000..ecffaf692 --- /dev/null +++ b/httemplate/edit/process/cable_provider.html @@ -0,0 +1,10 @@ +<& elements/process.html, + 'table' => 'cable_provider', + 'viewall_dir' => 'browse', +&> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +</%init> diff --git a/httemplate/edit/process/invoice_conf.html b/httemplate/edit/process/invoice_conf.html new file mode 100644 index 000000000..1d45e126f --- /dev/null +++ b/httemplate/edit/process/invoice_conf.html @@ -0,0 +1,21 @@ +<& elements/process.html, + 'table' => 'invoice_conf', + 'viewall_dir' => 'browse', + 'fields' => [ FS::invoice_conf->fields, 'modename', 'agentnum' ], + 'precheck_callback' => \&precheck_callback, +&> +<%init> +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']); + +sub precheck_callback { + my $cgi = shift; + $cgi->param('locale') =~ /^(\w*)$/; + my $locale = $1; + return mt('Language required') if $locale eq 'new'; # the user didn't choose + die "unknown locale $locale" if ( $locale and + !FS::Locales->locale_info($locale) ); +} +# invoice_conf itself knows to create/update invoice_mode if necessary, +# so nothing special here +</%init> diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html index be37d817d..1ffbcb9e8 100644 --- a/httemplate/elements/columnstart.html +++ b/httemplate/elements/columnstart.html @@ -1,6 +1,81 @@ +<%doc> +<table> + <& /elements/columnstart.html &> + <tr> ... </tr> + <tr> ... </tr> + <& /elements/columnnext.html &> + ... + <& /elements/columnend.html &> +</table> + +Pass 'aligned' => 1 to have corresponding rows in the columns line up. +</%doc> +% my $id = sprintf('table%08d', rand(100000000)); <TR> <TD CLASS="background" COLSPAN=99> - <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0> + <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>"> <TR> <TD VALIGN="top"> <TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0> +% if ( $aligned ) { +%# Instead of changing all the tr-* elements to sometimes output table +%# cells without wrapping them in a row, we're just going to completely +%# rebuild the table on the client side. +<script type="text/javascript"> +<&| onload.js &> + var table = document.getElementById('<%$id%>'); // has one row, always + var rows = []; // row contents, each containing + var n_rows = []; // rows in each subtable + var n_cols = []; // cols in each subtable + var total_rows = 0; // max(n_rows) + for(var i=0; i < table.rows[0].cells.length; i++) { + // these are cells created by columnstart/columnnext + // each contains a table, and nothing else + var subtable = table.rows[0].cells[i].children[0]; + n_rows[i] = subtable.rows.length; + if ( total_rows < n_rows[i] ) { + total_rows = n_rows[i]; + } + n_cols[i] = 0; + var subrows = []; // the rows of this table + for(var j=0; j < n_rows[i]; j++) { + // these are the actual tr-* rows within the table, and + // can contain multiple cells + subrows[j] = []; + var tr = subtable.rows[j]; + if ( n_cols[i] < tr.cells.length ) { + n_cols[i] = tr.cells.length; + } + for(var k=0; k < tr.cells.length; k++) { + subrows[j][k] = tr.cells[k]; + } + } // for(j) + rows[i] = subrows; + } // for(i) + var new_table = document.createElement('TABLE'); + for (var j = 0; j < total_rows; j++) { + var tr = document.createElement('TR'); + for (var i = 0; i < rows.length; i++) { // subtables + var k = 0; // subrow position + if ( j < n_rows[i] ) { // then subtable i has this row + for (k = 0; k < rows[i][j].length; k++) { // cells + tr.appendChild(rows[i][j][k]); + } + } // else k is just 0 + if ( k < n_cols[i] ) { // then we need a spacer + var spacer = document.createElement('TD'); + spacer.setAttribute('colspan', n_cols[i] - k); + tr.appendChild(spacer); + } + } // for(i); subtables + // tr is complete + new_table.appendChild(tr); + } // for(j); rows + table.parentNode.insertBefore( new_table, table ); + table.parentNode.removeChild(table); +</&> +</script> +% } # if $aligned +<%args> +$aligned => 0 +</%args> diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 8cbbd1742..8cb967518 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -197,6 +197,10 @@ foreach my $svcdb ( FS::part_svc->svc_tables() ) { [ $fsurl. 'search/report_svc_phone_usage.html', 'Total usage (minutes, and amount billed) for the specified time period, per phone number.', ]; + $report_svc{"${name} by state"} = + [ $fsurl. 'search/phone_state.html', + 'Current or historical phone services broken down by state.', + ]; } @@ -517,6 +521,7 @@ tie my %config_radius, 'Tie::IxHash', ; tie my %config_cable, 'Tie::IxHash', + 'Cable providers' => [ $fsurl.'browse/cable_provider.html', '' ], 'Cable modem models' => [ $fsurl.'browse/cable_model.html', '' ], ; @@ -600,6 +605,7 @@ $config_billing{'Billing events'} = [ $fsurl.'browse/part_event.html', 'Billing || $curuser->access_right('Edit global billing events'); if ( $curuser->access_right('Configuration') ) { #$config_billing{'Invoice events'} = [ $fsurl.'browse/part_bill_event.cgi', 'Deprecated, old-style actions for overdue invoices' ]; + $config_billing{'Invoice configurations'} = [ $fsurl.'browse/invoice_conf.html', 'Adjust invoice settings for special-purpose notices' ]; $config_billing{'Invoice templates'} = [ $fsurl.'browse/invoice_template.html', 'Edit templates for HTML, plaintext and typeset invoices' ]; $config_billing{'separator'} = ''; #its a separator! $config_billing{'Prepaid cards'} = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ]; diff --git a/httemplate/elements/select-cable_provider.html b/httemplate/elements/select-cable_provider.html new file mode 100644 index 000000000..9530b78c0 --- /dev/null +++ b/httemplate/elements/select-cable_provider.html @@ -0,0 +1,7 @@ +<% include( '/elements/select-table.html', + 'table' => 'cable_provider', + 'name_col' => 'provider', + 'empty_label' => 'Select provider', + @_, + ) +%> diff --git a/httemplate/elements/tr-select-cable_provider.html b/httemplate/elements/tr-select-cable_provider.html new file mode 100644 index 000000000..abb8564dc --- /dev/null +++ b/httemplate/elements/tr-select-cable_provider.html @@ -0,0 +1,12 @@ +% #if ( scalar(@domains) < 2 ) { +% #} else { + <TR> + <TD ALIGN="right"><% $opt{'label'} || 'Provider' %></TD> + <TD> + <% include( '/elements/select-cable_provider.html', %opt) %> + </TD> + </TR> +% #} +<%init> + my %opt = @_; +</%init> diff --git a/httemplate/elements/tr-select-invoice_mode.html b/httemplate/elements/tr-select-invoice_mode.html new file mode 100644 index 000000000..3dccdccc2 --- /dev/null +++ b/httemplate/elements/tr-select-invoice_mode.html @@ -0,0 +1,10 @@ +<& tr-select-table.html, + 'label' => 'Invoice mode', + 'table' => 'invoice_mode', + 'field' => 'modenum', + 'name_col' => 'modename', + 'agent_virt' => 1, + 'agent_null' => 1, + 'empty_label' => '(none)', + @_ +&> diff --git a/httemplate/misc/delete-invoice_conf.html b/httemplate/misc/delete-invoice_conf.html new file mode 100644 index 000000000..6cc6ddc95 --- /dev/null +++ b/httemplate/misc/delete-invoice_conf.html @@ -0,0 +1,19 @@ +<%init> +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right(['Edit templates', 'Edit global templates']); + +my ($query) = $cgi->keywords; +$query =~ /^(\d+)$/ or die "bad confnum"; +my $invoice_conf = FS::invoice_conf->by_key($1) + or die "couldn't find invoice_conf #$1"; +if ( !$curuser->access_right('Edit global templates') ) { + my $agentnum = FS::invoice_mode->by_key($invoice_conf->modenum)->agentnum; + die "access denied" + unless $curuser->agentnums_href->{$agentnum}; +} + +my $error = $invoice_conf->delete; # may also delete the invoice_mode +my $url = $p.'browse/invoice_conf.html'; +</%init> +<% $cgi->redirect($url) %> diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi index 269722f67..b24e0420f 100755 --- a/httemplate/misc/email-invoice.cgi +++ b/httemplate/misc/email-invoice.cgi @@ -12,7 +12,7 @@ my $invnum = $3; my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); die "Can't find invoice!\n" unless $cust_bill; -$cust_bill->email($template); +$cust_bill->email({ 'template' => $template }); my $custnum = $cust_bill->getfield('custnum'); diff --git a/httemplate/misc/fax-invoice.cgi b/httemplate/misc/fax-invoice.cgi index 2591fceb8..f72fc7eaf 100755 --- a/httemplate/misc/fax-invoice.cgi +++ b/httemplate/misc/fax-invoice.cgi @@ -12,7 +12,7 @@ my $invnum = $3; my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); die "Can't find invoice!\n" unless $cust_bill; -$cust_bill->fax_invoice($template); +$cust_bill->fax_invoice({ 'template' => $template }); my $custnum = $cust_bill->getfield('custnum'); diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi index aeef68795..5ce6e76df 100755 --- a/httemplate/misc/print-invoice.cgi +++ b/httemplate/misc/print-invoice.cgi @@ -12,7 +12,7 @@ my $invnum = $3; my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); die "Can't find invoice!\n" unless $cust_bill; -$cust_bill->print($template); +$cust_bill->print({ 'template' => $template}); my $custnum = $cust_bill->getfield('custnum'); diff --git a/httemplate/misc/send-invoice.cgi b/httemplate/misc/send-invoice.cgi index 32dfe276d..08dd0e01c 100644 --- a/httemplate/misc/send-invoice.cgi +++ b/httemplate/misc/send-invoice.cgi @@ -13,6 +13,10 @@ my $invnum = $cgi->param('invnum'); my $template = $cgi->param('template'); my $notice_name = $cgi->param('notice_name') if $cgi->param('notice_name'); my $method = $cgi->param('method'); +my $mode; +if ( $cgi->param('mode') =~ /^(\d+)$/ ) { + $mode = $1; +} $method .= '_invoice' if $method eq 'fax'; #! @@ -21,6 +25,7 @@ die "unknown method $method" unless $method{$method}; my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum}); die "Can't find invoice!\n" unless $cust_bill; +$cust_bill->set('mode' => $mode) if $mode; $cust_bill->$method({ 'template' => $template, 'notice_name' => $notice_name, }); diff --git a/httemplate/misc/xmlhttp-cust_main-duplicates.html b/httemplate/misc/xmlhttp-cust_main-duplicates.html index 7ee00af66..7cd463371 100644 --- a/httemplate/misc/xmlhttp-cust_main-duplicates.html +++ b/httemplate/misc/xmlhttp-cust_main-duplicates.html @@ -50,7 +50,9 @@ my $conf = new FS::Conf; my $sub = $cgi->param('sub'); my $hashref = decode_json($cgi->param('arg')); -my @cust_main = qsearch('cust_main', $hashref); +my $search = FS::cust_main->search($hashref); +#warn Dumper($search); +my @cust_main = qsearch( $search ); my $set_to_customer = <<EOF; var custnum_array = document.getElementsByName('dup_custnum'); diff --git a/httemplate/search/phone_state.html b/httemplate/search/phone_state.html new file mode 100644 index 000000000..67965b702 --- /dev/null +++ b/httemplate/search/phone_state.html @@ -0,0 +1,167 @@ +<& elements/search.html, + 'title' => $title, + 'name' => 'states', + 'query' => $query, + 'count_query' => $count_query, + 'count_addl' => $count_addl, + 'header' => [ 'State', # if we add more group fields, change this + 'Count', + 'Phone numbers' + ], + 'fields' => [ 'state', + 'num_svcnums', + $detail_sub + ], + 'html_init' => include('.head', $time), +&> +<%def .head> +% my $time = shift; +<FORM STYLE="display:inline" ACTION=<% $cgi->url %> METHOD="GET"> +Active phone services as of <& /elements/input-date-field.html, { + 'name' => 'date', + 'value' => $time, + 'format' => FS::Conf->new->config('date_format') || '%m/%d/%Y' +} &> +<INPUT TYPE="hidden" NAME="order_by" VALUE="<% $cgi->param('order_by') %>"> +<INPUT TYPE="submit" VALUE="Refresh"> +</FORM> +<BR> +<BR> +</%def> +<%init> +# svc_phone-specific for now; may change later +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" unless $curuser->access_right('Services: Phone numbers'); +my $title = 'Phone services by state'; + +my $time = time; +if ( $cgi->param('date') ) { + $time = parse_datetime($cgi->param('date')); + $title .= time2str(' (%b %o, %Y)', $time); +} + +my @tables = qw(svc_phone cust_svc cust_pkg cust_location cust_main); +my @pkeys = qw(svcnum svcnum pkgnum locationnum custnum); +my @h_tables = map "h_$_", @tables; + +my $addl_from = ''; +my @where; + +for(my $i = 0; $i < scalar(@tables); $i++) { + my $last_table = $h_tables[$i-1]; + my $pkey = $pkeys[$i]; + my $table = $tables[$i]; + my $h_table = $h_tables[$i]; + # alias the preceding table, and join to a subquery that finds the most + # recent change to $table.$pkey before $time + my $alias = $h_table; + my $inside = ''; + if ( $i > 0 ) { + $alias = "t$i"; + $inside = " AS $alias"; + } + $inside .= " + JOIN + (SELECT $pkey AS num, MAX(history_date) AS history_date + FROM $h_table + WHERE history_date <= $time AND + history_action IN ('insert', 'replace_new') + GROUP BY $pkey + ) AS mostrecent_$table + ON ($alias.$pkey = mostrecent_$table.num AND + $alias.history_date = mostrecent_$table.history_date AND + $alias.history_action IN ('insert', 'replace_new') + ) + LEFT JOIN + (SELECT $pkey AS num, MAX(history_date) AS history_date, 1 AS deleted + FROM $h_table + WHERE history_date <= $time AND + history_action = 'delete' + GROUP BY $pkey + ) AS deleted_$table + ON (mostrecent_$table.num = deleted_$table.num AND + mostrecent_$table.history_date < deleted_$table.history_date + ) +"; + # join to the preceding table if there is one, and filter out + # deleted records + if ( $i > 0 ) { + # special case to make pre-3.x data work; remove this later + if ( $table eq 'cust_main' ) { + $last_table = 'h_cust_pkg'; + } + $addl_from .= " + LEFT JOIN ( $h_table $inside ) AS $h_table + ON ($h_table.$pkey = $last_table.$pkey)"; + push @where, "$h_table.deleted IS NULL"; + } else { + $addl_from .= $inside; + push @where, "deleted_$table.deleted IS NULL"; + } +} + +# so that we know which services are still active +$addl_from .= " + LEFT JOIN svc_phone ON (h_svc_phone.svcnum = svc_phone.svcnum AND + h_svc_phone.phonenum = svc_phone.phonenum)"; + +#warn "\n\nJOIN EXPRESSION:\n$addl_from\n\n"; + +push @where, $curuser->agentnums_sql( + 'table' => 'h_cust_main', + 'null_right' => 'View/link unlinked services' +); +my $where = " WHERE ".join(' AND ', map "($_)", @where); + +# for pre-3.x data +my $group_field = 'COALESCE(h_cust_location.state, h_cust_main.ship_state, h_cust_main.state)'; + +my @select = ( + "$group_field AS state", + 'count(DISTINCT h_svc_phone.svcnum) AS num_svcnums', + # don't DISTINCT these (it reorders them) + "array_to_string(array_agg(h_svc_phone.phonenum), ',') AS all_phonenums", + "array_to_string(array_agg(h_svc_phone.svcnum), ',') AS all_svcnums", + "array_to_string(array_agg(svc_phone.svcnum), ',') AS active_svcnums", +); + +my $query = { + 'select' => join(',', @select), + 'table' => 'h_svc_phone', + 'addl_from' => $addl_from, + 'extra_sql' => " $where GROUP BY $group_field", +}; + +# DISTINCT on these because of cross-producting effects when a cust_pkg +# record (usually) was replaced more than once within one second. +my $count_query = + "SELECT COUNT(DISTINCT $group_field), COUNT(DISTINCT h_svc_phone.svcnum) ". + "FROM h_svc_phone $addl_from $where"; +my $count_addl = [ '%d phone services' ]; + +my $detail_sub = sub { + my $rec = shift; + warn Dumper $rec; + my @svcnums = split(',', $rec->all_svcnums); + my @phonenums = split(',', $rec->all_phonenums); + # identifies services that still exist with the same svcnum+phonenum + my %active = map { $_ => 1 } split(',', $rec->active_svcnums); + # make a single column of phonenums + my @return; + my %seen; + while (my $svcnum = shift @svcnums) { + my $phonenum = shift @phonenums; + next if $seen{$svcnum}; + $seen{$svcnum} = 1; + my $link = $active{$svcnum} ? + $p.'view/svc_phone.cgi?'.$svcnum : + ''; + push @return, [ { data => $phonenum, + link => $link, + data_style => ($active{$svcnum} ? '' : 'i') + } ]; + } + \@return; +}; + +</%init> diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi index 95ce60b1d..4822ab718 100755 --- a/httemplate/view/cust_bill.cgi +++ b/httemplate/view/cust_bill.cgi @@ -104,12 +104,35 @@ % my $br = 0; % if ( $cust_bill->num_cust_event ) { $br++; <A HREF="<%$p%>search/cust_event.html?invnum=<% $cust_bill->invnum %>">( <% mt('View invoice events') |h %> )</A> -% } +% } % if ( $cust_bill->num_cust_bill_event ) { $br++; <A HREF="<%$p%>search/cust_bill_event.cgi?invnum=<% $cust_bill->invnum %>">( <% mt('View deprecated, old-style invoice events') |h %> )</A> % } +% my @modes = grep {! $_->disabled} +% $cust_bill->cust_main->agent->invoice_modes; +% if ( @modes ) { +( <% mt('View as:') %> +<FORM STYLE="display:inline" ACTION="<% $cgi->url %>" METHOD="GET"> +<INPUT NAME="invnum" VALUE="<% $invnum %>" TYPE="hidden"> +<& /elements/select-table.html, + table => 'invoice_mode', + field => 'mode', + curr_value => scalar($cgi->param('mode')), + records => \@modes, + name_col => 'modename', + onchange => 'change_invoice_mode', + empty_label => '(default)', +&> ) +<SCRIPT TYPE="text/javascript"> +function change_invoice_mode(obj) { + obj.form.submit(); +} +</SCRIPT> +% $br++; +% } + <% $br ? '<BR><BR>' : '' %> % if ( $conf->exists('invoice_html') ) { @@ -126,7 +149,9 @@ my $curuser = $FS::CurrentUser::CurrentUser; die "access denied" unless $curuser->access_right('View invoices'); -my( $invnum, $template, $notice_name ); +my $conf = FS::Conf->new; + +my( $invnum, $mode, $template, $notice_name ); my($query) = $cgi->keywords; if ( $query =~ /^((.+)-)?(\d+)$/ ) { $template = $2; @@ -136,10 +161,9 @@ if ( $query =~ /^((.+)-)?(\d+)$/ ) { $invnum = $cgi->param('invnum'); $template = $cgi->param('template'); $notice_name = $cgi->param('notice_name'); + $mode = $cgi->param('mode'); } -my $conf = new FS::Conf; - my %opt = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email'), 'template' => $template, @@ -163,10 +187,13 @@ my $cust_bill = qsearchs({ }); die "Invoice #$invnum not found!" unless $cust_bill; +$cust_bill->set('mode' => $mode); + my $custnum = $cust_bill->custnum; my $display_custnum = $cust_bill->cust_main->display_custnum; my $link = "invnum=$invnum"; +$link .= ';mode=' . $mode if $mode; $link .= ';template='. uri_escape($template) if $template; $link .= ';notice_name='. $notice_name if $notice_name; diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi index 430c50c5f..391988190 100755 --- a/httemplate/view/cust_main.cgi +++ b/httemplate/view/cust_main.cgi @@ -147,7 +147,6 @@ function areyousure(href, message) { % if ( $br ) { <BR><BR> % } -</%doc> %my $signupurl = $conf->config('signupurl'); %if ( $signupurl ) { diff --git a/httemplate/view/cust_statement.html b/httemplate/view/cust_statement.html index 3e1345ed5..5d37b3167 100755 --- a/httemplate/view/cust_statement.html +++ b/httemplate/view/cust_statement.html @@ -35,10 +35,10 @@ % if ( $conf->exists('invoice_html') ) { - <% join('', $cust_statement->print_html('', $templatename) ) %> + <% join('', $cust_statement->print_html('template' => $templatename) ) %> % } else { - <PRE><% join('', $cust_statement->print_text('', $templatename) ) %></PRE> + <PRE><% join('', $cust_statement->print_text('template' => $templatename) ) %></PRE> % } <% include('/elements/footer.html') %> diff --git a/httemplate/view/elements/cust_bill-typeset b/httemplate/view/elements/cust_bill-typeset index 00f503fbb..778e538d1 100644 --- a/httemplate/view/elements/cust_bill-typeset +++ b/httemplate/view/elements/cust_bill-typeset @@ -6,7 +6,7 @@ die "access denied" my $type = shift; -my( $invnum, $template, $notice_name ); +my( $invnum, $mode, $template, $notice_name ); my($query) = $cgi->keywords; if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore? $template = $2; @@ -16,7 +16,8 @@ if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore? $invnum = $cgi->param('invnum'); $invnum =~ s/\.pdf//i; #probably not necessary anymore $template = $cgi->param('template'); - $notice_name = ( $cgi->param('notice_name') || 'Invoice' ); + $notice_name = $cgi->param('notice_name'); + $mode = $cgi->param('mode'); } my $conf = new FS::Conf; @@ -36,6 +37,8 @@ my $cust_bill = qsearchs({ }); die "Invoice #$invnum not found!" unless $cust_bill; +$cust_bill->set(mode => $mode); + my $method = "print_$type"; my $content = $cust_bill->$method(\%opt); diff --git a/ng_selfservice/.freeside.class.php.swp b/ng_selfservice/.freeside.class.php.swp Binary files differdeleted file mode 100644 index 5c3952439..000000000 --- a/ng_selfservice/.freeside.class.php.swp +++ /dev/null diff --git a/ng_selfservice/.index.php.swp b/ng_selfservice/.index.php.swp Binary files differdeleted file mode 100644 index 50c9cfbc5..000000000 --- a/ng_selfservice/.index.php.swp +++ /dev/null diff --git a/ng_selfservice/.logout.php.swp b/ng_selfservice/.logout.php.swp Binary files differdeleted file mode 100644 index ec27faaed..000000000 --- a/ng_selfservice/.logout.php.swp +++ /dev/null diff --git a/ng_selfservice/.main.php.swp b/ng_selfservice/.main.php.swp Binary files differdeleted file mode 100644 index cc5562690..000000000 --- a/ng_selfservice/.main.php.swp +++ /dev/null diff --git a/ng_selfservice/.password.php.swp b/ng_selfservice/.password.php.swp Binary files differdeleted file mode 100644 index e1e968f1f..000000000 --- a/ng_selfservice/.password.php.swp +++ /dev/null diff --git a/ng_selfservice/.payment.php.swp b/ng_selfservice/.payment.php.swp Binary files differdeleted file mode 100644 index 2b705a31f..000000000 --- a/ng_selfservice/.payment.php.swp +++ /dev/null diff --git a/ng_selfservice/.payment_ach.php.swp b/ng_selfservice/.payment_ach.php.swp Binary files differdeleted file mode 100644 index 1a87a2d34..000000000 --- a/ng_selfservice/.payment_ach.php.swp +++ /dev/null diff --git a/ng_selfservice/.payment_cc.php.swp b/ng_selfservice/.payment_cc.php.swp Binary files differdeleted file mode 100644 index 369d104d1..000000000 --- a/ng_selfservice/.payment_cc.php.swp +++ /dev/null diff --git a/ng_selfservice/.payment_paypal.php.swp b/ng_selfservice/.payment_paypal.php.swp Binary files differdeleted file mode 100644 index 3abff2f4d..000000000 --- a/ng_selfservice/.payment_paypal.php.swp +++ /dev/null diff --git a/ng_selfservice/.payment_webpay.php.swp b/ng_selfservice/.payment_webpay.php.swp Binary files differdeleted file mode 100644 index 6ef3df9d9..000000000 --- a/ng_selfservice/.payment_webpay.php.swp +++ /dev/null diff --git a/ng_selfservice/.personal.php.swp b/ng_selfservice/.personal.php.swp Binary files differdeleted file mode 100644 index f5e8c23c1..000000000 --- a/ng_selfservice/.personal.php.swp +++ /dev/null diff --git a/ng_selfservice/.process_login.php.swp b/ng_selfservice/.process_login.php.swp Binary files differdeleted file mode 100644 index c530f11d6..000000000 --- a/ng_selfservice/.process_login.php.swp +++ /dev/null diff --git a/ng_selfservice/.process_ticket_create.php.swp b/ng_selfservice/.process_ticket_create.php.swp Binary files differdeleted file mode 100644 index c286792cd..000000000 --- a/ng_selfservice/.process_ticket_create.php.swp +++ /dev/null diff --git a/ng_selfservice/.services.php.swp b/ng_selfservice/.services.php.swp Binary files differdeleted file mode 100644 index e063e406b..000000000 --- a/ng_selfservice/.services.php.swp +++ /dev/null diff --git a/ng_selfservice/.services_new.php.swp b/ng_selfservice/.services_new.php.swp Binary files differdeleted file mode 100644 index 8d0c657af..000000000 --- a/ng_selfservice/.services_new.php.swp +++ /dev/null diff --git a/ng_selfservice/.ticket.php.swp b/ng_selfservice/.ticket.php.swp Binary files differdeleted file mode 100644 index e9b25032b..000000000 --- a/ng_selfservice/.ticket.php.swp +++ /dev/null diff --git a/ng_selfservice/.ticket_create.php.swp b/ng_selfservice/.ticket_create.php.swp Binary files differdeleted file mode 100644 index 65b00fe06..000000000 --- a/ng_selfservice/.ticket_create.php.swp +++ /dev/null diff --git a/ng_selfservice/.tickets.php.swp b/ng_selfservice/.tickets.php.swp Binary files differdeleted file mode 100644 index 7b4d67b48..000000000 --- a/ng_selfservice/.tickets.php.swp +++ /dev/null diff --git a/ng_selfservice/.tickets_resolved.php.swp b/ng_selfservice/.tickets_resolved.php.swp Binary files differdeleted file mode 100644 index 1b3c634f0..000000000 --- a/ng_selfservice/.tickets_resolved.php.swp +++ /dev/null diff --git a/ng_selfservice/.usage.php.swp b/ng_selfservice/.usage.php.swp Binary files differdeleted file mode 100644 index 61fd4fa4d..000000000 --- a/ng_selfservice/.usage.php.swp +++ /dev/null diff --git a/ng_selfservice/.usage_cdr.php.swp b/ng_selfservice/.usage_cdr.php.swp Binary files differdeleted file mode 100644 index 83c270a83..000000000 --- a/ng_selfservice/.usage_cdr.php.swp +++ /dev/null diff --git a/ng_selfservice/.usage_data.php.swp b/ng_selfservice/.usage_data.php.swp Binary files differdeleted file mode 100644 index e5a9272b0..000000000 --- a/ng_selfservice/.usage_data.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.card.php.swp b/ng_selfservice/elements/.card.php.swp Binary files differdeleted file mode 100644 index 15d30cefd..000000000 --- a/ng_selfservice/elements/.card.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.check.php.swp b/ng_selfservice/elements/.check.php.swp Binary files differdeleted file mode 100644 index fe08303ff..000000000 --- a/ng_selfservice/elements/.check.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.error.php.swp b/ng_selfservice/elements/.error.php.swp Binary files differdeleted file mode 100644 index 1a6eb2833..000000000 --- a/ng_selfservice/elements/.error.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.header.php.swp b/ng_selfservice/elements/.header.php.swp Binary files differdeleted file mode 100644 index 237177069..000000000 --- a/ng_selfservice/elements/.header.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.menu.php.swp b/ng_selfservice/elements/.menu.php.swp Binary files differdeleted file mode 100644 index 0c29ff942..000000000 --- a/ng_selfservice/elements/.menu.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.menu_footer.php.swp b/ng_selfservice/elements/.menu_footer.php.swp Binary files differdeleted file mode 100644 index 4bd2b30a8..000000000 --- a/ng_selfservice/elements/.menu_footer.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.session.php.swp b/ng_selfservice/elements/.session.php.swp Binary files differdeleted file mode 100644 index ddd013701..000000000 --- a/ng_selfservice/elements/.session.php.swp +++ /dev/null diff --git a/ng_selfservice/elements/.ticketlist.php.swp b/ng_selfservice/elements/.ticketlist.php.swp Binary files differdeleted file mode 100644 index db3d0fe4a..000000000 --- a/ng_selfservice/elements/.ticketlist.php.swp +++ /dev/null diff --git a/ng_selfservice/js/.menu.js.swp b/ng_selfservice/js/.menu.js.swp Binary files differdeleted file mode 100644 index 8df94a9c4..000000000 --- a/ng_selfservice/js/.menu.js.swp +++ /dev/null diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm index 4a6bfda88..2dc16e3f3 100644 --- a/rt/lib/RT/Interface/Web.pm +++ b/rt/lib/RT/Interface/Web.pm @@ -2070,7 +2070,6 @@ sub _ProcessUpdateMessageRecipients { if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{Requestor} ); push @txn_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses; - } push @txn_squelch, @{$args{ARGSRef}{SquelchMailTo}} if $args{ARGSRef}{SquelchMailTo}; @@ -2092,6 +2091,39 @@ sub _ProcessUpdateMessageRecipients { } } +sub ProcessAttachments { + my %args = ( + ARGSRef => {}, + @_ + ); + + my $ARGSRef = $args{ARGSRef} || {}; + # deal with deleting uploaded attachments + foreach my $key ( keys %$ARGSRef ) { + if ( $key =~ m/^DeleteAttach-(.+)$/ ) { + delete $session{'Attachments'}{$1}; + } + $session{'Attachments'} = { %{ $session{'Attachments'} || {} } }; + } + + # store the uploaded attachment in session + if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} ) + { # attachment? + my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' ); + + my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}"); + $session{'Attachments'} = + { %{ $session{'Attachments'} || {} }, $file_path => $attachment, }; + } + + # delete temporary storage entry to make WebUI clean + unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} ) + { + delete $session{'Attachments'}; + } +} + + =head2 MakeMIMEEntity PARAMHASH Takes a paramhash Subject, Body and AttachmentFieldName. @@ -2174,37 +2206,6 @@ sub MakeMIMEEntity { } -sub ProcessAttachments { - my %args = ( - ARGSRef => {}, - @_ - ); - - my $ARGSRef = $args{ARGSRef} || {}; - # deal with deleting uploaded attachments - foreach my $key ( keys %$ARGSRef ) { - if ( $key =~ m/^DeleteAttach-(.+)$/ ) { - delete $session{'Attachments'}{$1}; - } - $session{'Attachments'} = { %{ $session{'Attachments'} || {} } }; - } - - # store the uploaded attachment in session - if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} ) - { # attachment? - my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' ); - - my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}"); - $session{'Attachments'} = - { %{ $session{'Attachments'} || {} }, $file_path => $attachment, }; - } - - # delete temporary storage entry to make WebUI clean - unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} ) - { - delete $session{'Attachments'}; - } -} =head2 ParseDateToISO diff --git a/rt/lib/RT/URI/freeside/Internal.pm b/rt/lib/RT/URI/freeside/Internal.pm index b0962860d..d1479b5f9 100644 --- a/rt/lib/RT/URI/freeside/Internal.pm +++ b/rt/lib/RT/URI/freeside/Internal.pm @@ -152,6 +152,9 @@ sub AsStringLong { if ( $table eq 'cust_main' ) { my $rec = $self->_FreesideGetRecord(); + if (!$rec) { + return '<I>Customer #'.$self->{'fspkey'}.' (not found)</I>'; + } return '<A HREF="' . $self->HREF . '">' . small_custview( $rec->{'_object'}, scalar(FS::Conf->new->config('countrydefault')), @@ -192,21 +195,37 @@ sub CustomerResolver { } elsif ( $self->{fstable} eq 'cust_svc' ) { my $rec = $self->_FreesideGetRecord(); - return if !$rec; - my $cust_pkg = $rec->{'_object'}->cust_pkg; - if ( $cust_pkg ) { - my $URI = RT::URI->new($self->CurrentUser); - $URI->FromURI('freeside://freeside/cust_main/'.$cust_pkg->custnum); - return $URI->Resolver; + if ($rec) { + my $cust_pkg = $rec->{'_object'}->cust_pkg; + if ( $cust_pkg ) { + my $URI = RT::URI->new($self->CurrentUser); + $URI->FromURI('freeside://freeside/cust_main/'.$cust_pkg->custnum); + return $URI->Resolver; + } } + return; } return; } sub CustomerInfo { my $self = shift; - $self = $self->CustomerResolver or return; - my $rec = $self->_FreesideGetRecord() or return; + $self = $self->CustomerResolver; + my $rec = $self->_FreesideGetRecord() if $self; + if (!$rec) { + # AsStringLong will report an error; + # here, just avoid breaking things + my $error = { + AgentName => '', + CustomerClass => '', + CustomerTags => [], + Referral => '', + InvoiceEmail => '', + BillingType => '', + }; + return $error; + } + my $cust_main = delete $rec->{_object}; my $agent = $cust_main->agent; my $class = $cust_main->cust_class; diff --git a/rt/share/html/Ticket/Elements/Customers b/rt/share/html/Ticket/Elements/Customers index d90ef1c44..fed678380 100644 --- a/rt/share/html/Ticket/Elements/Customers +++ b/rt/share/html/Ticket/Elements/Customers @@ -43,10 +43,12 @@ while (my $link = $customers->Next) { } elsif ( $uri =~ /cust_svc\/(\d+)/ ) { my $svc = $link->TargetURI->Resolver; my $cust = $svc->CustomerResolver; - my $custnum = $cust->{fspkey}; - $cust_main{$custnum} ||= $cust; - $cust_svc{$custnum} ||= []; - push @{$cust_svc{$custnum}}, $svc; + if ( $cust ) { + my $custnum = $cust->{fspkey}; + $cust_main{$custnum} ||= $cust if $cust; + $cust_svc{$custnum} ||= []; + push @{$cust_svc{$custnum}}, $svc if $svc; + } } } @custnums = sort { $a <=> $b } keys %cust_main; diff --git a/rt/share/html/Ticket/Update.html b/rt/share/html/Ticket/Update.html index 26a37e80a..8a3d8e30d 100755 --- a/rt/share/html/Ticket/Update.html +++ b/rt/share/html/Ticket/Update.html @@ -290,6 +290,7 @@ if ( $ARGS{'SubmitTicket'} ) { my %squelched = ProcessTransactionSquelching( \%ARGS ); $ARGS{'SquelchMailTo'} = [keys %squelched] if keys %squelched; +warn @{ $ARGS{'SquelchMailTo'} } if $ARGS{'SquelchMailTo'}; my $CFs = $TicketObj->TransactionCustomFields; my $ValidCFs = $m->comp( @@ -311,6 +312,7 @@ if ( $ARGS{'SubmitTicket'} ) { ); $checks_failure = 1 unless $status; } +warn @{ $ARGS{'SquelchMailTo'} } if $ARGS{'SquelchMailTo'}; # check email addresses for RT's { |