5 use FS::UID qw( getotaker );
6 use FS::Record qw( qsearch qsearchs );
13 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
15 # because they load configuraion by setting FS::UID::callback (see TODO)
20 @ISA = qw( FS::Record );
24 FS::cust_pkg - Object methods for cust_pkg objects
30 $record = new FS::cust_pkg \%hash;
31 $record = new FS::cust_pkg { 'column' => 'value' };
33 $error = $record->insert;
35 $error = $new_record->replace($old_record);
37 $error = $record->delete;
39 $error = $record->check;
41 $error = $record->cancel;
43 $error = $record->suspend;
45 $error = $record->unsuspend;
47 $part_pkg = $record->part_pkg;
49 @labels = $record->labels;
51 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
52 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
56 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
57 inherits from FS::Record. The following fields are currently supported:
61 =item pkgnum - primary key (assigned automatically for new billing items)
63 =item custnum - Customer (see L<FS::cust_main>)
65 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
77 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
81 Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
82 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
91 Create a new billing item. To add the item to the database, see L<"insert">.
95 sub table { 'cust_pkg'; }
99 Adds this billing item to the database ("Orders" the item). If there is an
100 error, returns the error, otherwise returns false.
105 # custnum might not have have been defined in sub check (for one-shot new
106 # customers), so check it here instead
108 my $error = $self->ut_number('custnum');
109 return $error if $error
111 return "Unknown customer"
112 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
114 $self->SUPER::insert;
120 Currently unimplemented. You don't want to delete billing items, because there
121 would then be no record the customer ever purchased the item. Instead, see
127 return "Can't delete cust_pkg records!";
130 =item replace OLD_RECORD
132 Replaces the OLD_RECORD with this one in the database. If there is an error,
133 returns the error, otherwise returns false.
135 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
137 Changing pkgpart may have disasterous effects. See the order subroutine.
139 setup and bill are normally updated by calling the bill method of a customer
140 object (see L<FS::cust_main>).
142 suspend is normally updated by the suspend and unsuspend methods.
144 cancel is normally updated by the cancel method (and also the order subroutine
150 my( $new, $old ) = ( shift, shift );
152 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
153 return "Can't change otaker!" if $old->otaker ne $new->otaker;
154 return "Can't change setup once it exists!"
155 if $old->getfield('setup') &&
156 $old->getfield('setup') != $new->getfield('setup');
157 #some logic for bill, susp, cancel?
159 $new->SUPER::replace($old);
164 Checks all fields to make sure this is a valid billing item. If there is an
165 error, returns the error, otherwise returns false. Called by the insert and
174 $self->ut_numbern('pkgnum')
175 || $self->ut_numbern('custnum')
176 || $self->ut_number('pkgpart')
177 || $self->ut_numbern('setup')
178 || $self->ut_numbern('bill')
179 || $self->ut_numbern('susp')
180 || $self->ut_numbern('cancel')
182 return $error if $error;
184 if ( $self->custnum ) {
185 return "Unknown customer"
186 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
189 return "Unknown pkgpart"
190 unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
192 $self->otaker(getotaker) unless $self->otaker;
193 $self->otaker =~ /^(\w{0,16})$/ or return "Illegal otaker";
201 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
202 in this package, then cancels the package itself (sets the cancel field to
205 If there is an error, returns the error, otherwise returns false.
213 local $SIG{HUP} = 'IGNORE';
214 local $SIG{INT} = 'IGNORE';
215 local $SIG{QUIT} = 'IGNORE';
216 local $SIG{TERM} = 'IGNORE';
217 local $SIG{TSTP} = 'IGNORE';
218 local $SIG{PIPE} = 'IGNORE';
220 foreach my $cust_svc (
221 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
223 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
225 $part_svc->svcdb =~ /^([\w\-]+)$/
226 or return "Illegal svcdb value in part_svc!";
228 require "FS/$svcdb.pm";
230 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
232 $error = $svc->cancel;
233 return "Error cancelling service: $error" if $error;
234 $error = $svc->delete;
235 return "Error deleting service: $error" if $error;
238 $error = $cust_svc->delete;
239 return "Error deleting cust_svc: $error" if $error;
243 unless ( $self->getfield('cancel') ) {
244 my %hash = $self->hash;
245 $hash{'cancel'} = time;
246 my $new = new FS::cust_pkg ( \%hash );
247 $error = $new->replace($self);
248 return $error if $error;
256 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
257 package, then suspends the package itself (sets the susp field to now).
259 If there is an error, returns the error, otherwise returns false.
267 local $SIG{HUP} = 'IGNORE';
268 local $SIG{INT} = 'IGNORE';
269 local $SIG{QUIT} = 'IGNORE';
270 local $SIG{TERM} = 'IGNORE';
271 local $SIG{TSTP} = 'IGNORE';
272 local $SIG{PIPE} = 'IGNORE';
274 foreach my $cust_svc (
275 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
277 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
279 $part_svc->svcdb =~ /^([\w\-]+)$/
280 or return "Illegal svcdb value in part_svc!";
282 require "FS/$svcdb.pm";
284 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
286 $error = $svc->suspend;
287 return $error if $error;
292 unless ( $self->getfield('susp') ) {
293 my %hash = $self->hash;
294 $hash{'susp'} = time;
295 my $new = new FS::cust_pkg ( \%hash );
296 $error = $new->replace($self);
297 return $error if $error;
305 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
306 package, then unsuspends the package itself (clears the susp field).
308 If there is an error, returns the error, otherwise returns false.
316 local $SIG{HUP} = 'IGNORE';
317 local $SIG{INT} = 'IGNORE';
318 local $SIG{QUIT} = 'IGNORE';
319 local $SIG{TERM} = 'IGNORE';
320 local $SIG{TSTP} = 'IGNORE';
321 local $SIG{PIPE} = 'IGNORE';
323 foreach my $cust_svc (
324 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
326 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
328 $part_svc->svcdb =~ /^([\w\-]+)$/
329 or return "Illegal svcdb value in part_svc!";
331 require "FS/$svcdb.pm";
333 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
335 $error = $svc->unsuspend;
336 return $error if $error;
341 unless ( ! $self->getfield('susp') ) {
342 my %hash = $self->hash;
344 my $new = new FS::cust_pkg ( \%hash );
345 $error = $new->replace($self);
346 return $error if $error;
354 Returns the definition for this billing item, as an FS::part_pkg object (see
361 qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
366 Returns a list of lists, calling the label method for all services
367 (see L<FS::cust_svc>) of this billing item.
373 map { [ $_->label ] } qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
382 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF ]
384 CUSTNUM is a customer (see L<FS::cust_main>)
386 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
387 L<FS::part_pkg>) to order for this customer. Duplicates are of course
390 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
391 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
392 new billing items. An error is returned if this is not possible (see
398 my($custnum,$pkgparts,$remove_pkgnums)=@_;
401 # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
403 my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
404 my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
405 my %part_pkg = %{ $agent->pkgpart_hashref };
409 # for those packages being removed:
410 #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::Record
411 # objects (table eq 'cust_svc')
413 foreach $pkgnum ( @{$remove_pkgnums} ) {
415 foreach $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) {
416 push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc;
422 # for those packages the customer is purchasing:
423 # @{$pkgparts} is a list of said packages, by pkgpart
424 # @cust_svc is a corresponding list of lists of FS::Record objects
426 foreach $pkgpart ( @{$pkgparts} ) {
427 return "Customer not permitted to purchase pkgpart $pkgpart!"
428 unless $part_pkg{$pkgpart};
431 ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : ();
432 } map { $_->svcpart } qsearch('pkg_svc', { 'pkgpart' => $pkgpart })
436 #check for leftover services
437 foreach (keys %svcnum) {
438 next unless @{ $svcnum{$_} };
439 return "Leftover services, svcpart $_: svcnum ".
440 join(', ', map { $_->svcnum } @{ $svcnum{$_} } );
443 #no leftover services, let's make changes.
445 local $SIG{HUP} = 'IGNORE';
446 local $SIG{INT} = 'IGNORE';
447 local $SIG{QUIT} = 'IGNORE';
448 local $SIG{TERM} = 'IGNORE';
449 local $SIG{TSTP} = 'IGNORE';
450 local $SIG{PIPE} = 'IGNORE';
452 #first cancel old packages
454 foreach $pkgnum ( @{$remove_pkgnums} ) {
455 my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
456 die "Package $pkgnum not found to remove!" unless $old;
457 my(%hash) = $old->hash;
458 $hash{'cancel'}=time;
459 my($new) = new FS::cust_pkg ( \%hash );
460 my($error)=$new->replace($old);
461 die "Couldn't update package $pkgnum: $error" if $error;
464 #now add new packages, changing cust_svc records if necessary
466 while ($pkgpart=shift @{$pkgparts} ) {
468 my($new) = new FS::cust_pkg ( {
469 'custnum' => $custnum,
470 'pkgpart' => $pkgpart,
472 my($error) = $new->insert;
473 die "Couldn't insert new cust_pkg record: $error" if $error;
474 my($pkgnum)=$new->getfield('pkgnum');
477 foreach $cust_svc ( @{ shift @cust_svc } ) {
478 my(%hash) = $cust_svc->hash;
479 $hash{'pkgnum'}=$pkgnum;
480 my($new) = new FS::cust_svc ( \%hash );
481 my($error)=$new->replace($cust_svc);
482 die "Couldn't link old service to new package: $error" if $error;
493 $Id: cust_pkg.pm,v 1.3 1999-11-08 21:38:38 ivan Exp $
497 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
499 In sub order, the @pkgparts array (passed by reference) is clobbered.
501 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
502 method to pass dates to the recur_prog expression, it should do so.
504 FS::svc_acct, FS::svc_acct_sm, and FS::svc_domain are loaded via 'use' at
505 compile time, rather than via 'require' in sub { setup, suspend, unsuspend,
506 cancel } because they use %FS::UID::callback to load configuration values.
507 Probably need a subroutine which decides what to do based on whether or not
508 we've fetched the user yet, rather than a hash. See FS::UID and the TODO.
512 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>
513 , L<FS::pkg_svc>, schema.html from the base documentation