From: Ivan Kohler Date: Wed, 9 Oct 2013 06:00:26 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=fe4515eb37d76849dd08c62782d86bc7ba311dcd;hp=f2766e203e1aa144d046a26cf13e01e1f5b00f64 Merge branch 'master' of git.freeside.biz:/home/git/freeside Conflicts: FS/FS/cust_main.pm --- 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 the TomTom website 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 the TomTom website 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 => "DEPRECATED", type => "text" }, { key => "apachemachine", section => "deprecated", description => "DEPRECATED", type => "text" }, { key => "apachemachines", section => "deprecated", description => "DEPRECATED", 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) set to the customer's locale. + +If the "mode" pseudo-field is set on the object, the configuration handle +will be an L 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