sales tax districts, #15089
[freeside.git] / FS / FS / cust_location.pm
1 package FS::cust_location;
2
3 use strict;
4 use base qw( FS::geocode_Mixin FS::Record );
5 use Locale::Country;
6 use FS::UID qw( dbh );
7 use FS::Record qw( qsearch ); #qsearchs );
8 use FS::Conf;
9 use FS::prospect_main;
10 use FS::cust_main;
11 use FS::cust_main_county;
12
13 =head1 NAME
14
15 FS::cust_location - Object methods for cust_location records
16
17 =head1 SYNOPSIS
18
19   use FS::cust_location;
20
21   $record = new FS::cust_location \%hash;
22   $record = new FS::cust_location { 'column' => 'value' };
23
24   $error = $record->insert;
25
26   $error = $new_record->replace($old_record);
27
28   $error = $record->delete;
29
30   $error = $record->check;
31
32 =head1 DESCRIPTION
33
34 An FS::cust_location object represents a customer location.  FS::cust_location
35 inherits from FS::Record.  The following fields are currently supported:
36
37 =over 4
38
39 =item locationnum
40
41 primary key
42
43 =item custnum
44
45 custnum
46
47 =item address1
48
49 Address line one (required)
50
51 =item address2
52
53 Address line two (optional)
54
55 =item city
56
57 City
58
59 =item county
60
61 County (optional, see L<FS::cust_main_county>)
62
63 =item state
64
65 State (see L<FS::cust_main_county>)
66
67 =item zip
68
69 Zip
70
71 =item country
72
73 Country (see L<FS::cust_main_county>)
74
75 =item geocode
76
77 Geocode
78
79 =item district
80
81 Tax district code (optional)
82
83 =item disabled
84
85 Disabled flag; set to 'Y' to disable the location.
86
87 =back
88
89 =head1 METHODS
90
91 =over 4
92
93 =item new HASHREF
94
95 Creates a new location.  To add the location to the database, see L<"insert">.
96
97 Note that this stores the hash reference, not a distinct copy of the hash it
98 points to.  You can ask the object for a copy with the I<hash> method.
99
100 =cut
101
102 sub table { 'cust_location'; }
103
104 =item insert
105
106 Adds this record to the database.  If there is an error, returns the error,
107 otherwise returns false.
108
109 =cut
110
111 sub insert {
112   my $self = shift;
113   my $error = $self->SUPER::insert(@_);
114
115   #false laziness with cust_main, will go away eventually
116   my $conf = new FS::Conf;
117   if ( !$error and $conf->config('tax_district_method') ) {
118
119     my $queue = new FS::queue {
120       'job' => 'FS::geocode_Mixin::process_district_update'
121     };
122     $error = $queue->insert( ref($self), $self->locationnum );
123
124   }
125
126   $error || '';
127 }
128
129 =item delete
130
131 Delete this record from the database.
132
133 =item replace OLD_RECORD
134
135 Replaces the OLD_RECORD with this one in the database.  If there is an error,
136 returns the error, otherwise returns false.
137
138 =cut
139
140 sub replace {
141   my $self = shift;
142   my $old = shift;
143   $old ||= $self->replace_old;
144   my $error = $self->SUPER::replace($old);
145
146   #false laziness with cust_main, will go away eventually
147   my $conf = new FS::Conf;
148   if ( !$error and $conf->config('tax_district_method') 
149     and $self->get('address1') ne $old->get('address1') ) {
150
151     my $queue = new FS::queue {
152       'job' => 'FS::geocode_Mixin::process_district_update'
153     };
154     $error = $queue->insert( ref($self), $self->locationnum );
155
156   }
157
158   $error || '';
159 }
160
161
162 =item check
163
164 Checks all fields to make sure this is a valid location.  If there is
165 an error, returns the error, otherwise returns false.  Called by the insert
166 and replace methods.
167
168 =cut
169
170 #some false laziness w/cust_main, but since it should eventually lose these
171 #fields anyway...
172 sub check {
173   my $self = shift;
174
175   my $error = 
176     $self->ut_numbern('locationnum')
177     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
178     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
179     || $self->ut_text('address1')
180     || $self->ut_textn('address2')
181     || $self->ut_text('city')
182     || $self->ut_textn('county')
183     || $self->ut_textn('state')
184     || $self->ut_country('country')
185     || $self->ut_zip('zip', $self->country)
186     || $self->ut_coordn('latitude')
187     || $self->ut_coordn('longitude')
188     || $self->ut_enum('coord_auto', [ '', 'Y' ])
189     || $self->ut_alphan('location_type')
190     || $self->ut_textn('location_number')
191     || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
192     || $self->ut_alphan('geocode')
193     || $self->ut_alphan('district')
194   ;
195   return $error if $error;
196
197   $self->set_coord
198     unless $self->latitude && $self->longitude;
199
200   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
201   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
202
203   my $conf = new FS::Conf;
204   return 'Location kind is required'
205     if $self->prospectnum
206     && $conf->exists('prospect_main-alt_address_format')
207     && ! $self->location_kind;
208
209   unless ( qsearch('cust_main_county', {
210     'country' => $self->country,
211     'state'   => '',
212    } ) ) {
213     return "Unknown state/county/country: ".
214       $self->state. "/". $self->county. "/". $self->country
215       unless qsearch('cust_main_county',{
216         'state'   => $self->state,
217         'county'  => $self->county,
218         'country' => $self->country,
219       } );
220   }
221
222   $self->SUPER::check;
223 }
224
225 =item country_full
226
227 Returns this locations's full country name
228
229 =cut
230
231 sub country_full {
232   my $self = shift;
233   code2country($self->country);
234 }
235
236 =item line
237
238 Synonym for location_label
239
240 =cut
241
242 sub line {
243   my $self = shift;
244   $self->location_label;
245 }
246
247 =item has_ship_address
248
249 Returns false since cust_location objects do not have a separate shipping
250 address.
251
252 =cut
253
254 sub has_ship_address {
255   '';
256 }
257
258 =item location_hash
259
260 Returns a list of key/value pairs, with the following keys: address1, address2,
261 city, county, state, zip, country, geocode, location_type, location_number,
262 location_kind.
263
264 =cut
265
266 =item move_to HASHREF
267
268 Takes a hashref with one or more cust_location fields.  Creates a duplicate 
269 of the existing location with all fields set to the values in the hashref.  
270 Moves all packages that use the existing location to the new one, then sets 
271 the "disabled" flag on the old location.  Returns nothing on success, an 
272 error message on error.
273
274 =cut
275
276 sub move_to {
277   my $old = shift;
278   my $hashref = shift;
279
280   local $SIG{HUP} = 'IGNORE';
281   local $SIG{INT} = 'IGNORE';
282   local $SIG{QUIT} = 'IGNORE';
283   local $SIG{TERM} = 'IGNORE';
284   local $SIG{TSTP} = 'IGNORE';
285   local $SIG{PIPE} = 'IGNORE';
286
287   my $oldAutoCommit = $FS::UID::AutoCommit;
288   local $FS::UID::AutoCommit = 0;
289   my $dbh = dbh;
290   my $error = '';
291
292   my $new = FS::cust_location->new({
293       $old->location_hash,
294       'custnum'     => $old->custnum,
295       'prospectnum' => $old->prospectnum,
296       %$hashref
297     });
298   $error = $new->insert;
299   if ( $error ) {
300     $dbh->rollback if $oldAutoCommit;
301     return "Error creating location: $error";
302   }
303
304   my @pkgs = qsearch('cust_pkg', { 
305       'locationnum' => $old->locationnum,
306       'cancel' => '' 
307     });
308   foreach my $cust_pkg (@pkgs) {
309     $error = $cust_pkg->change(
310       'locationnum' => $new->locationnum,
311       'keep_dates'  => 1
312     );
313     if ( $error and not ref($error) ) {
314       $dbh->rollback if $oldAutoCommit;
315       return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
316     }
317   }
318
319   $old->disabled('Y');
320   $error = $old->replace;
321   if ( $error ) {
322     $dbh->rollback if $oldAutoCommit;
323     return "Error disabling old location: $error";
324   }
325
326   $dbh->commit if $oldAutoCommit;
327   return;
328 }
329
330 =item alternize
331
332 Attempts to parse data for location_type and location_number from address1
333 and address2.
334
335 =cut
336
337 sub alternize {
338   my $self = shift;
339
340   return '' if $self->get('location_type')
341             || $self->get('location_number');
342
343   my %parse;
344   if ( 1 ) { #ikano, switch on via config
345     { no warnings 'void';
346       eval { 'use FS::part_export::ikano;' };
347       die $@ if $@;
348     }
349     %parse = FS::part_export::ikano->location_types_parse;
350   } else {
351     %parse = (); #?
352   }
353
354   foreach my $from ('address1', 'address2') {
355     foreach my $parse ( keys %parse ) {
356       my $value = $self->get($from);
357       if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
358         $self->set('location_type', $parse{$parse});
359         $self->set('location_number', $2);
360         $self->set($from, $value);
361         return '';
362       }
363     }
364   }
365
366   #nothing matched, no changes
367   $self->get('address2')
368     ? "Can't parse unit type and number from address2"
369     : '';
370 }
371
372 =item dealternize
373
374 Moves data from location_type and location_number to the end of address1.
375
376 =cut
377
378 sub dealternize {
379   my $self = shift;
380
381   #false laziness w/geocode_Mixin.pm::line
382   my $lt = $self->get('location_type');
383   if ( $lt ) {
384
385     my %location_type;
386     if ( 1 ) { #ikano, switch on via config
387       { no warnings 'void';
388         eval { 'use FS::part_export::ikano;' };
389         die $@ if $@;
390       }
391       %location_type = FS::part_export::ikano->location_types;
392     } else {
393       %location_type = (); #?
394     }
395
396     $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
397     $self->location_type('');
398   }
399
400   if ( length($self->location_number) ) {
401     $self->address1( $self->address1. ' '. $self->location_number );
402     $self->location_number('');
403   }
404  
405   '';
406 }
407
408 =back
409
410 =head1 BUGS
411
412 Not yet used for cust_main billing and shipping addresses.
413
414 =head1 SEE ALSO
415
416 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
417 schema.html from the base documentation.
418
419 =cut
420
421 1;
422