5 use FS::UID qw( getotaker );
6 use FS::Record qw( qsearch qsearchs );
11 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
13 # because they load configuraion by setting FS::UID::callback (see TODO)
18 @ISA = qw( FS::Record );
22 FS::cust_pkg - Object methods for cust_pkg objects
28 $record = new FS::cust_pkg \%hash;
29 $record = new FS::cust_pkg { 'column' => 'value' };
31 $error = $record->insert;
33 $error = $new_record->replace($old_record);
35 $error = $record->delete;
37 $error = $record->check;
39 $error = $record->cancel;
41 $error = $record->suspend;
43 $error = $record->unsuspend;
45 $part_pkg = $record->part_pkg;
47 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
48 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
52 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
53 inherits from FS::Record. The following fields are currently supported:
57 =item pkgnum - primary key (assigned automatically for new billing items)
59 =item custnum - Customer (see L<FS::cust_main>)
61 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
73 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
77 Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
78 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
87 Create a new billing item. To add the item to the database, see L<"insert">.
91 sub table { 'cust_pkg'; }
95 Adds this billing item to the database ("Orders" the item). If there is an
96 error, returns the error, otherwise returns false.
100 Currently unimplemented. You don't want to delete billing items, because there
101 would then be no record the customer ever purchased the item. Instead, see
107 return "Can't delete cust_pkg records!";
110 =item replace OLD_RECORD
112 Replaces the OLD_RECORD with this one in the database. If there is an error,
113 returns the error, otherwise returns false.
115 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
117 Changing pkgpart may have disasterous effects. See the order subroutine.
119 setup and bill are normally updated by calling the bill method of a customer
120 object (see L<FS::cust_main>).
122 suspend is normally updated by the suspend and unsuspend methods.
124 cancel is normally updated by the cancel method (and also the order subroutine
130 my( $new, $old ) = ( shift, shift );
132 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
133 return "Can't change otaker!" if $old->otaker ne $new->otaker;
134 return "Can't change setup once it exists!"
135 if $old->getfield('setup') &&
136 $old->getfield('setup') != $new->getfield('setup');
137 #some logic for bill, susp, cancel?
139 $new->SUPER::replace($old);
144 Checks all fields to make sure this is a valid billing item. If there is an
145 error, returns the error, otherwise returns false. Called by the insert and
154 $self->ut_numbern('pkgnum')
155 || $self->ut_number('custnum')
156 || $self->ut_number('pkgpart')
157 || $self->ut_numbern('setup')
158 || $self->ut_numbern('bill')
159 || $self->ut_numbern('susp')
160 || $self->ut_numbern('cancel')
162 return $error if $error;
164 return "Unknown customer"
165 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
167 return "Unknown pkgpart"
168 unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
170 $self->otaker(getotaker) unless $self->otaker;
171 $self->otaker =~ /^(\w{0,16})$/ or return "Illegal otaker";
179 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
180 in this package, then cancels the package itself (sets the cancel field to
183 If there is an error, returns the error, otherwise returns false.
191 local $SIG{HUP} = 'IGNORE';
192 local $SIG{INT} = 'IGNORE';
193 local $SIG{QUIT} = 'IGNORE';
194 local $SIG{TERM} = 'IGNORE';
195 local $SIG{TSTP} = 'IGNORE';
196 local $SIG{PIPE} = 'IGNORE';
198 foreach my $cust_svc (
199 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
201 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
203 $part_svc->svcdb =~ /^([\w\-]+)$/
204 or return "Illegal svcdb value in part_svc!";
206 require "FS/$svcdb.pm";
208 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
210 $error = $svc->cancel;
211 return "Error cancelling service: $error" if $error;
212 $error = $svc->delete;
213 return "Error deleting service: $error" if $error;
216 $error = $cust_svc->delete;
217 return "Error deleting cust_svc: $error" if $error;
221 unless ( $self->getfield('cancel') ) {
222 my %hash = $self->hash;
223 $hash{'cancel'} = time;
224 my $new = new FS::cust_pkg ( \%hash );
225 $error = $new->replace($self);
226 return $error if $error;
234 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
235 package, then suspends the package itself (sets the susp field to now).
237 If there is an error, returns the error, otherwise returns false.
245 local $SIG{HUP} = 'IGNORE';
246 local $SIG{INT} = 'IGNORE';
247 local $SIG{QUIT} = 'IGNORE';
248 local $SIG{TERM} = 'IGNORE';
249 local $SIG{TSTP} = 'IGNORE';
250 local $SIG{PIPE} = 'IGNORE';
252 foreach my $cust_svc (
253 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
255 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
257 $part_svc->svcdb =~ /^([\w\-]+)$/
258 or return "Illegal svcdb value in part_svc!";
260 require "FS/$svcdb.pm";
262 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
264 $error = $svc->suspend;
265 return $error if $error;
270 unless ( $self->getfield('susp') ) {
271 my %hash = $self->hash;
272 $hash{'susp'} = time;
273 my $new = new FS::cust_pkg ( \%hash );
274 $error = $new->replace($self);
275 return $error if $error;
283 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
284 package, then unsuspends the package itself (clears the susp field).
286 If there is an error, returns the error, otherwise returns false.
294 local $SIG{HUP} = 'IGNORE';
295 local $SIG{INT} = 'IGNORE';
296 local $SIG{QUIT} = 'IGNORE';
297 local $SIG{TERM} = 'IGNORE';
298 local $SIG{TSTP} = 'IGNORE';
299 local $SIG{PIPE} = 'IGNORE';
301 foreach my $cust_svc (
302 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
304 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
306 $part_svc->svcdb =~ /^([\w\-]+)$/
307 or return "Illegal svcdb value in part_svc!";
309 require "FS/$svcdb.pm";
311 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
313 $error = $svc->unsuspend;
314 return $error if $error;
319 unless ( ! $self->getfield('susp') ) {
320 my %hash = $self->hash;
322 my $new = new FS::cust_pkg ( \%hash );
323 $error = $new->replace($self);
324 return $error if $error;
332 Returns the definition for this billing item, as an FS::part_pkg object (see
339 qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
348 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF ]
350 CUSTNUM is a customer (see L<FS::cust_main>)
352 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
353 L<FS::part_pkg>) to order for this customer. Duplicates are of course
356 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
357 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
358 new billing items. An error is returned if this is not possible (see
364 my($custnum,$pkgparts,$remove_pkgnums)=@_;
368 # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
369 my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
370 my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
373 foreach $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
374 my($pkgpart)=$type_pkgs->pkgpart;
375 $part_pkg{$pkgpart}++;
381 # for those packages being removed:
382 #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::Record
383 # objects (table eq 'cust_svc')
385 foreach $pkgnum ( @{$remove_pkgnums} ) {
387 foreach $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) {
388 push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc;
394 # for those packages the customer is purchasing:
395 # @{$pkgparts} is a list of said packages, by pkgpart
396 # @cust_svc is a corresponding list of lists of FS::Record objects
398 foreach $pkgpart ( @{$pkgparts} ) {
399 return "Customer not permitted to purchase pkgpart $pkgpart!"
400 unless $part_pkg{$pkgpart};
403 ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : ();
405 qsearchs('part_pkg',{'pkgpart'=>$pkgpart})->getfield('services')
410 #check for leftover services
411 foreach (keys %svcnum) {
412 next unless @{ $svcnum{$_} };
413 return "Leftover services!";
416 #no leftover services, let's make changes.
418 local $SIG{HUP} = 'IGNORE';
419 local $SIG{INT} = 'IGNORE';
420 local $SIG{QUIT} = 'IGNORE';
421 local $SIG{TERM} = 'IGNORE';
422 local $SIG{TSTP} = 'IGNORE';
423 local $SIG{PIPE} = 'IGNORE';
425 #first cancel old packages
427 foreach $pkgnum ( @{$remove_pkgnums} ) {
428 my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
429 die "Package $pkgnum not found to remove!" unless $old;
430 my(%hash) = $old->hash;
431 $hash{'cancel'}=time;
432 my($new) = new FS::cust_pkg ( \%hash );
433 my($error)=$new->replace($old);
434 die "Couldn't update package $pkgnum: $error" if $error;
437 #now add new packages, changing cust_svc records if necessary
439 while ($pkgpart=shift @{$pkgparts} ) {
441 my($new) = new FS::cust_pkg ( {
442 'custnum' => $custnum,
443 'pkgpart' => $pkgpart,
445 my($error) = $new->insert;
446 die "Couldn't insert new cust_pkg record: $error" if $error;
447 my($pkgnum)=$new->getfield('pkgnum');
450 foreach $cust_svc ( @{ shift @cust_svc } ) {
451 my(%hash) = $cust_svc->hash;
452 $hash{'pkgnum'}=$pkgnum;
453 my($new) = new FS::cust_svc ( \%hash );
454 my($error)=$new->replace($cust_svc);
455 die "Couldn't link old service to new package: $error" if $error;
466 $Id: cust_pkg.pm,v 1.6 1999-01-25 12:26:12 ivan Exp $
470 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
472 In sub order, the @pkgparts array (passed by reference) is clobbered.
474 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
475 method to pass dates to the recur_prog expression, it should do so.
477 FS::svc_acct, FS::svc_acct_sm, and FS::svc_domain are loaded via 'use' at
478 compile time, rather than via 'require' in sub { setup, suspend, unsuspend,
479 cancel } because they use %FS::UID::callback to load configuration values.
480 Probably need a subroutine which decides what to do based on whether or not
481 we've fetched the user yet, rather than a hash. See FS::UID and the TODO.
485 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>
486 , L<FS::pkg_svc>, schema.html from the base documentation
490 ivan@voicenet.com 97-jul-1 - 21
492 fixed for new agent->agent_type->type_pkgs in &order ivan@sisd.com 98-mar-7
494 pod ivan@sisd.com 98-sep-21
496 $Log: cust_pkg.pm,v $
497 Revision 1.6 1999-01-25 12:26:12 ivan
498 yet more mod_perl stuff
500 Revision 1.5 1999/01/18 21:58:07 ivan
501 esthetic: eq and ne were used in a few places instead of == and !=
503 Revision 1.4 1998/12/29 11:59:45 ivan
504 mostly properly OO, some work still to be done with svc_ stuff
506 Revision 1.3 1998/11/15 13:01:35 ivan
507 allow pkgpart changing (for per-customer custom pricing). warn about it in doc
509 Revision 1.2 1998/11/12 03:42:45 ivan