1 package FS::quotation_pkg;
4 use base qw( FS::TemplateItem_Mixin FS::Record );
5 use FS::Record qw( qsearch qsearchs dbh );
9 use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
10 use FS::quotation_pkg_detail;
11 use List::Util qw(sum);
15 FS::quotation_pkg - Object methods for quotation_pkg records
19 use FS::quotation_pkg;
21 $record = new FS::quotation_pkg \%hash;
22 $record = new FS::quotation_pkg { 'column' => 'value' };
24 $error = $record->insert;
26 $error = $new_record->replace($old_record);
28 $error = $record->delete;
30 $error = $record->check;
34 An FS::quotation_pkg object represents a quotation package.
35 FS::quotation_pkg inherits from FS::Record. The following fields are currently
46 pkgpart (L<FS::part_pkg>) of the package
50 locationnum (L<FS::cust_location>) where the package will be in service
54 expected start date for the package, as a timestamp
66 'Y' to waive the setup fee
70 The amount per package that will be charged in setup/one-time fees.
74 The amount per package that will be charged per billing cycle.
84 Creates a new quotation package. To add the quotation package to the database,
87 Note that this stores the hash reference, not a distinct copy of the hash it
88 points to. You can ask the object for a copy with the I<hash> method.
92 sub table { 'quotation_pkg'; }
94 sub display_table { 'quotation_pkg'; }
96 #forget it, just overriding cust_bill_pkg_display entirely
97 #sub display_table_orderby { 'quotationpkgnum'; } # something else?
98 # # (for invoice display order)
100 sub discount_table { 'quotation_pkg_discount'; }
101 sub detail_table { 'quotation_pkg_detail'; }
105 Adds this record to the database. If there is an error, returns the error,
106 otherwise returns false.
111 my ($self, %options) = @_;
114 my $oldAutoCommit = $FS::UID::AutoCommit;
115 local $FS::UID::AutoCommit = 0;
117 #false laziness w/cust_main::Packages::order_pkg
118 if ( $options{'locationnum'} and $options{'locationnum'} != -1 ) {
120 $self->locationnum($options{'locationnum'});
122 } elsif ( $options{'cust_location'} ) {
124 my $error = $options{'cust_location'}->find_or_insert;
126 $dbh->rollback if $oldAutoCommit;
127 return "inserting cust_location (transaction rolled back): $error";
129 $self->locationnum($options{'cust_location'}->locationnum);
133 my $error = $self->SUPER::insert;
135 if ( !$error and $self->discountnum ) {
136 $error = $self->insert_discount;
137 $error .= ' (setting discount)' if $error;
140 # update $self and any discounts with their amounts
142 $error = $self->estimate;
143 $error .= ' (calculating charges)' if $error;
147 $dbh->rollback if $oldAutoCommit;
150 $dbh->commit if $oldAutoCommit;
157 Delete this record from the database.
165 my $oldAutoCommit = $FS::UID::AutoCommit;
166 local $FS::UID::AutoCommit = 0;
168 my $error = $self->delete_details;
170 $dbh->rollback if $oldAutoCommit;
174 foreach ($self->quotation_pkg_discount, $self->quotation_pkg_tax) {
177 $dbh->rollback if $oldAutoCommit;
178 return $error . ' (deleting discount)';
182 $error = $self->SUPER::delete;
184 $dbh->rollback if $oldAutoCommit;
187 $dbh->commit if $oldAutoCommit;
193 =item replace OLD_RECORD
195 Replaces the OLD_RECORD with this one in the database. If there is an error,
196 returns the error, otherwise returns false.
200 Checks all fields to make sure this is a valid quotation package. If there is
201 an error, returns the error, otherwise returns false. Called by the insert
210 $self->ut_numbern('quotationpkgnum')
211 || $self->ut_foreign_key( 'quotationnum', 'quotation', 'quotationnum' )
212 || $self->ut_foreign_key( 'pkgpart', 'part_pkg', 'pkgpart' )
213 || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum' )
214 || $self->ut_numbern('start_date')
215 || $self->ut_numbern('contract_end')
216 || $self->ut_numbern('quantity')
217 || $self->ut_moneyn('unitsetup')
218 || $self->ut_moneyn('unitrecur')
219 || $self->ut_enum('waive_setup', [ '', 'Y'] )
222 if ($self->locationnum eq '') {
223 # use the customer default
224 my $quotation = $self->quotation;
225 if ($quotation->custnum) {
226 $self->set('locationnum', $quotation->cust_main->ship_locationnum);
227 } elsif ($quotation->prospectnum) {
228 # use the first non-disabled location for that prospect
229 my $cust_location = qsearchs('cust_location',
230 { prospectnum => $quotation->prospectnum,
232 $self->set('locationnum', $cust_location->locationnum) if $cust_location;
233 } # else the quotation is invalid
236 return $error if $error;
243 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart } );
248 $self->part_pkg->pkg;
253 Update the quotation_pkg record with the estimated setup and recurring
254 charges for the package. Returns nothing on success, or an error message
261 my $part_pkg = $self->part_pkg;
262 my $quantity = $self->quantity || 1;
263 my ($unitsetup, $unitrecur);
264 # calculate base fees
265 if ( $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'} ) {
268 $unitsetup = $part_pkg->option('setup_fee',1) || '0.00'; # XXX 3.x only
270 if ( $self->{'_NO_RECUR_KLUDGE'} ) {
273 $unitrecur = $part_pkg->base_recur;
278 $self->set('unitsetup', $unitsetup);
279 $self->set('unitrecur', $unitrecur);
280 my $error = $self->replace;
281 return $error if $error;
283 # semi-duplicates calc_discount
284 my $setup_discount = 0;
285 my $recur_discount = 0;
287 my %setup_discounts; # quotationpkgdiscountnum => amount
288 my %recur_discounts; # quotationpkgdiscountnum => amount
290 # XXX the order of applying discounts is ill-defined, which matters
291 # if there are percentage and amount discounts on the same package.
293 # but right now there can only be one discount on any package, so
295 foreach my $pkg_discount ($self->quotation_pkg_discount) {
297 my $discount = $pkg_discount->discount;
298 my $this_setup_discount = 0;
299 my $this_recur_discount = 0;
301 if ( $discount->percent > 0 ) {
303 if ( $discount->setup ) {
304 $this_setup_discount = ($discount->percent * $unitsetup / 100);
306 $this_recur_discount = ($discount->percent * $unitrecur / 100);
308 } elsif ( $discount->amount > 0 ) {
310 my $discount_left = $discount->amount;
311 if ( $discount->setup ) {
312 if ( $discount_left > $unitsetup - $setup_discount ) {
313 # then discount the setup to zero
314 $discount_left -= $unitsetup - $setup_discount;
315 $this_setup_discount = $unitsetup - $setup_discount;
317 # not enough discount to fully cover the setup
318 $this_setup_discount = $discount_left;
322 # same logic for recur
323 if ( $discount_left > $unitrecur - $recur_discount ) {
324 $this_recur_discount = $unitrecur - $recur_discount;
326 $this_recur_discount = $discount_left;
331 # increment the total discountage
332 $setup_discount += $this_setup_discount;
333 $recur_discount += $this_recur_discount;
334 # and update the pkg_discount object
335 $pkg_discount->set('setup_amount', sprintf('%.2f', $setup_discount));
336 $pkg_discount->set('recur_amount', sprintf('%.2f', $recur_discount));
337 my $error = $pkg_discount->replace;
338 return $error if $error;
344 =item insert_discount
346 Associates this package with a discount (see L<FS::cust_pkg_discount>,
347 possibly inserting a new discount on the fly (see L<FS::discount>). Properties
348 of the discount will be taken from this object.
352 sub insert_discount {
353 #my ($self, %options) = @_;
356 my $quotation_pkg_discount = FS::quotation_pkg_discount->new( {
357 'quotationpkgnum' => $self->quotationpkgnum,
358 'discountnum' => $self->discountnum,
359 #for the create a new discount case
360 '_type' => $self->discountnum__type,
361 'amount' => $self->discountnum_amount,
362 'percent' => $self->discountnum_percent,
363 'months' => $self->discountnum_months,
364 'setup' => $self->discountnum_setup,
367 $quotation_pkg_discount->insert;
372 my @pkg_discounts = $self->pkg_discount;
373 return if @pkg_discounts == 0;
378 description => $self->mt('Discount'),
382 ext_description => \@ext,
383 # maybe should show quantity/unit discount?
385 foreach my $pkg_discount (@pkg_discounts) {
386 push @ext, $pkg_discount->description;
387 $d->{setup_amount} -= $pkg_discount->setup_amount;
388 $d->{recur_amount} -= $pkg_discount->recur_amount;
390 $d->{setup_amount} *= $self->quantity || 1;
391 $d->{recur_amount} *= $self->quantity || 1;
392 $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
399 ($self->unitsetup - sum(0, map { $_->setup_amount } $self->pkg_discount))
400 * ($self->quantity || 1);
403 sub setup_show_zero {
405 return $self->part_pkg->setup_show_zero;
410 ($self->unitrecur - sum(0, map { $_->recur_amount } $self->pkg_discount))
411 * ($self->quantity || 1);
414 sub recur_show_zero {
416 return $self->part_pkg->recur_show_zero;
421 Deletes all quotation_pkgs_details associated with this pkg (see L<FS::quotation_pkg_detail>).
428 my $oldAutoCommit = $FS::UID::AutoCommit;
429 local $FS::UID::AutoCommit = 0;
432 foreach my $detail ( qsearch('quotation_pkg_detail',{ 'quotationpkgnum' => $self->quotationpkgnum }) ) {
433 my $error = $detail->delete;
435 $dbh->rollback if $oldAutoCommit;
436 return "error removing old detail: $error";
440 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
445 =item set_details PARAM
447 Sets new quotation details for this package (see L<FS::quotation_pkg_detail>),
448 removing existing details.
450 Recognizes the following parameters:
452 details - arrayref of strings, one for each new detail
454 copy_on_order - if true, sets copy_on_order flag on new details
456 If there is an error, returns the error, otherwise returns false.
464 $opt{'details'} ||= [];
465 my @details = @{$opt{'details'}};
467 my $oldAutoCommit = $FS::UID::AutoCommit;
468 local $FS::UID::AutoCommit = 0;
471 my $error = $self->delete_details;
473 $dbh->rollback if $oldAutoCommit;
477 foreach my $detail ( @details ) {
478 my $quotation_pkg_detail = new FS::quotation_pkg_detail {
479 'quotationpkgnum' => $self->quotationpkgnum,
481 'copy_on_order' => $opt{'copy_on_order'} ? 'Y' : '',
483 $error = $quotation_pkg_detail->insert;
485 $dbh->rollback if $oldAutoCommit;
486 return "error adding new detail: $error";
490 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
499 =item cust_bill_pkg_display [ type => TYPE ]
503 sub cust_bill_pkg_display {
504 my ( $self, %opt ) = @_;
506 my $type = $opt{type} if exists $opt{type};
507 return () if $type eq 'U'; #quotations don't have usage
509 if ( $self->get('display') ) {
510 return ( grep { defined($type) ? ($type eq $_->type) : 1 }
511 @{ $self->get('display') }
516 my $setup = $self->new($self->hashref);
517 $setup->{'_NO_RECUR_KLUDGE'} = 1;
518 $setup->{'type'} = 'S';
519 my $recur = $self->new($self->hashref);
520 $recur->{'_NO_SETUP_KLUDGE'} = 1;
521 $recur->{'type'} = 'R';
523 if ( $type eq 'S' ) {
525 } elsif ( $type eq 'R' ) {
528 #return ($setup, $recur);
538 Returns the customer (L<FS::cust_main> object).
544 my $quotation = FS::quotation->by_key($self->quotationnum) or return '';
545 $quotation->cust_main;
548 sub tax_locationnum {
557 FS::quotation->by_key($self->quotationnum);
560 sub quotation_pkg_detail {
562 sort { $a->detailnum <=> $b->detailnum }
563 qsearch('quotation_pkg_detail', { quotationpkgnum => $self->quotationpkgnum });
566 sub quotation_pkg_discount {
568 qsearch('quotation_pkg_discount', { quotationpkgnum => $self->quotationpkgnum });
571 sub quotation_pkg_tax {
573 qsearch('quotation_pkg_tax', { quotationpkgnum => $self->quotationpkgnum });
578 $self->locationnum ? qsearchs('cust_location', { locationnum => $self->locationnum }) : '';
584 my @quotation_pkg_without_location =
585 qsearch( 'quotation_pkg', { locationnum => '' } );
586 if (@quotation_pkg_without_location) {
587 warn "setting default location on quotation_pkg records\n";
588 foreach my $quotation_pkg (@quotation_pkg_without_location) {
589 # check() will fix this
590 my $error = $quotation_pkg->replace;
592 die "quotation #".$quotation_pkg->quotationnum.": $error\n";
603 Doesn't support fees, or add-on packages.
607 L<FS::Record>, schema.html from the base documentation.