1 package FS::quotation_pkg;
2 use base qw( FS::TemplateItem_Mixin FS::Record );
5 use FS::Record qw( qsearchs dbh ); #qsearch
7 use FS::quotation_pkg_discount; #so its loaded when TemplateItem_Mixin needs it
8 use List::Util qw(sum);
12 FS::quotation_pkg - Object methods for quotation_pkg records
16 use FS::quotation_pkg;
18 $record = new FS::quotation_pkg \%hash;
19 $record = new FS::quotation_pkg { 'column' => 'value' };
21 $error = $record->insert;
23 $error = $new_record->replace($old_record);
25 $error = $record->delete;
27 $error = $record->check;
31 An FS::quotation_pkg object represents a quotation package.
32 FS::quotation_pkg inherits from FS::Record. The following fields are currently
43 pkgpart (L<FS::part_pkg>) of the package
47 locationnum (L<FS::cust_location>) where the package will be in service
51 expected start date for the package, as a timestamp
63 'Y' to waive the setup fee
67 The amount per package that will be charged in setup/one-time fees.
71 The amount per package that will be charged per billing cycle.
81 Creates a new quotation package. To add the quotation package to the database,
84 Note that this stores the hash reference, not a distinct copy of the hash it
85 points to. You can ask the object for a copy with the I<hash> method.
89 sub table { 'quotation_pkg'; }
91 sub display_table { 'quotation_pkg'; }
93 #forget it, just overriding cust_bill_pkg_display entirely
94 #sub display_table_orderby { 'quotationpkgnum'; } # something else?
95 # # (for invoice display order)
97 sub discount_table { 'quotation_pkg_discount'; }
101 Adds this record to the database. If there is an error, returns the error,
102 otherwise returns false.
107 my ($self, %options) = @_;
110 my $oldAutoCommit = $FS::UID::AutoCommit;
111 local $FS::UID::AutoCommit = 0;
113 my $error = $self->SUPER::insert;
115 if ( !$error and $self->discountnum ) {
116 $error = $self->insert_discount;
117 $error .= ' (setting discount)' if $error;
120 # update $self and any discounts with their amounts
122 $error = $self->estimate;
123 $error .= ' (calculating charges)' if $error;
127 $dbh->rollback if $oldAutoCommit;
130 $dbh->commit if $oldAutoCommit;
137 Delete this record from the database.
145 my $oldAutoCommit = $FS::UID::AutoCommit;
146 local $FS::UID::AutoCommit = 0;
148 foreach ($self->quotation_pkg_discount, $self->quotation_pkg_tax) {
149 my $error = $_->delete;
151 $dbh->rollback if $oldAutoCommit;
152 return $error . ' (deleting discount)';
156 my $error = $self->SUPER::delete;
158 $dbh->rollback if $oldAutoCommit;
161 $dbh->commit if $oldAutoCommit;
167 =item replace OLD_RECORD
169 Replaces the OLD_RECORD with this one in the database. If there is an error,
170 returns the error, otherwise returns false.
174 Checks all fields to make sure this is a valid quotation package. If there is
175 an error, returns the error, otherwise returns false. Called by the insert
184 $self->ut_numbern('quotationpkgnum')
185 || $self->ut_foreign_key( 'quotationnum', 'quotation', 'quotationnum' )
186 || $self->ut_foreign_key( 'pkgpart', 'part_pkg', 'pkgpart' )
187 || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum' )
188 || $self->ut_numbern('start_date')
189 || $self->ut_numbern('contract_end')
190 || $self->ut_numbern('quantity')
191 || $self->ut_moneyn('unitsetup')
192 || $self->ut_moneyn('unitrecur')
193 || $self->ut_enum('waive_setup', [ '', 'Y'] )
196 if ($self->locationnum eq '') {
197 # use the customer default
198 my $quotation = $self->quotation;
199 if ($quotation->custnum) {
200 $self->set('locationnum', $quotation->cust_main->ship_locationnum);
201 } elsif ($quotation->prospectnum) {
202 # use the first non-disabled location for that prospect
203 my $cust_location = qsearchs('cust_location',
204 { prospectnum => $quotation->prospectnum,
206 $self->set('locationnum', $cust_location->locationnum) if $cust_location;
207 } # else the quotation is invalid
210 return $error if $error;
215 #it looks redundant with a v4.x+ auto-generated method, but need to override
216 # FS::TemplateItem_Mixin's version
219 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart } );
224 $self->part_pkg->pkg;
229 Update the quotation_pkg record with the estimated setup and recurring
230 charges for the package. Returns nothing on success, or an error message
237 my $part_pkg = $self->part_pkg;
238 my $quantity = $self->quantity || 1;
239 my ($unitsetup, $unitrecur);
240 # calculate base fees
241 if ( $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'} ) {
244 $unitsetup = $part_pkg->base_setup;
246 if ( $self->{'_NO_RECUR_KLUDGE'} ) {
249 $unitrecur = $part_pkg->base_recur;
254 $self->set('unitsetup', $unitsetup);
255 $self->set('unitrecur', $unitrecur);
256 my $error = $self->replace;
257 return $error if $error;
259 # semi-duplicates calc_discount
260 my $setup_discount = 0;
261 my $recur_discount = 0;
263 my %setup_discounts; # quotationpkgdiscountnum => amount
264 my %recur_discounts; # quotationpkgdiscountnum => amount
266 # XXX the order of applying discounts is ill-defined, which matters
267 # if there are percentage and amount discounts on the same package.
269 # but right now there can only be one discount on any package, so
271 foreach my $pkg_discount ($self->quotation_pkg_discount) {
273 my $discount = $pkg_discount->discount;
274 my $this_setup_discount = 0;
275 my $this_recur_discount = 0;
277 if ( $discount->percent > 0 ) {
279 if ( $discount->setup ) {
280 $this_setup_discount = ($discount->percent * $unitsetup / 100);
282 $this_recur_discount = ($discount->percent * $unitrecur / 100);
284 } elsif ( $discount->amount > 0 ) {
286 my $discount_left = $discount->amount;
287 if ( $discount->setup ) {
288 if ( $discount_left > $unitsetup - $setup_discount ) {
289 # then discount the setup to zero
290 $discount_left -= $unitsetup - $setup_discount;
291 $this_setup_discount = $unitsetup - $setup_discount;
293 # not enough discount to fully cover the setup
294 $this_setup_discount = $discount_left;
298 # same logic for recur
299 if ( $discount_left > $unitrecur - $recur_discount ) {
300 $this_recur_discount = $unitrecur - $recur_discount;
302 $this_recur_discount = $discount_left;
307 # increment the total discountage
308 $setup_discount += $this_setup_discount;
309 $recur_discount += $this_recur_discount;
310 # and update the pkg_discount object
311 $pkg_discount->set('setup_amount', sprintf('%.2f', $setup_discount));
312 $pkg_discount->set('recur_amount', sprintf('%.2f', $recur_discount));
313 my $error = $pkg_discount->replace;
314 return $error if $error;
320 =item insert_discount
322 Associates this package with a discount (see L<FS::cust_pkg_discount>,
323 possibly inserting a new discount on the fly (see L<FS::discount>). Properties
324 of the discount will be taken from this object.
328 sub insert_discount {
329 #my ($self, %options) = @_;
332 my $quotation_pkg_discount = FS::quotation_pkg_discount->new( {
333 'quotationpkgnum' => $self->quotationpkgnum,
334 'discountnum' => $self->discountnum,
335 #for the create a new discount case
336 '_type' => $self->discountnum__type,
337 'amount' => $self->discountnum_amount,
338 'percent' => $self->discountnum_percent,
339 'months' => $self->discountnum_months,
340 'setup' => $self->discountnum_setup,
343 $quotation_pkg_discount->insert;
348 my @pkg_discounts = $self->pkg_discount;
349 return if @pkg_discounts == 0;
354 description => $self->mt('Discount'),
358 ext_description => \@ext,
359 # maybe should show quantity/unit discount?
361 foreach my $pkg_discount (@pkg_discounts) {
362 push @ext, $pkg_discount->description;
363 $d->{setup_amount} -= $pkg_discount->setup_amount;
364 $d->{recur_amount} -= $pkg_discount->recur_amount;
366 $d->{setup_amount} *= $self->quantity || 1;
367 $d->{recur_amount} *= $self->quantity || 1;
368 $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
375 ($self->unitsetup - sum(0, map { $_->setup_amount } $self->pkg_discount))
376 * ($self->quantity || 1);
381 ($self->unitrecur - sum(0, map { $_->recur_amount } $self->pkg_discount))
382 * ($self->quantity || 1);
385 =item part_pkg_currency_option OPTIONNAME
387 Returns a two item list consisting of the currency of this quotation's customer
388 or prospect, if any, and a value for the provided option. If the customer or
389 prospect has a currency, the value is the option value the given name and the
390 currency (see L<FS::part_pkg_currency>). Otherwise, if the customer or
391 prospect has no currency, is the regular option value for the given name (see
392 L<FS::part_pkg_option>).
396 #false laziness w/cust_pkg->part_pkg_currency_option
397 sub part_pkg_currency_option {
398 my( $self, $optionname ) = @_;
399 my $part_pkg = $self->part_pkg;
400 my $prospect_or_customer = $self->cust_main || $self->prospect_main;
401 if ( my $currency = $prospect_or_customer->currency ) {
402 ($currency, $part_pkg->part_pkg_currency_option($currency, $optionname) );
404 ('', $part_pkg->option($optionname) );
409 =item cust_bill_pkg_display [ type => TYPE ]
413 sub cust_bill_pkg_display {
414 my ( $self, %opt ) = @_;
416 my $type = $opt{type} if exists $opt{type};
417 return () if $type eq 'U'; #quotations don't have usage
419 if ( $self->get('display') ) {
420 return ( grep { defined($type) ? ($type eq $_->type) : 1 }
421 @{ $self->get('display') }
426 my $setup = $self->new($self->hashref);
427 $setup->{'_NO_RECUR_KLUDGE'} = 1;
428 $setup->{'type'} = 'S';
429 my $recur = $self->new($self->hashref);
430 $recur->{'_NO_SETUP_KLUDGE'} = 1;
431 $recur->{'type'} = 'R';
433 if ( $type eq 'S' ) {
434 sub tax_locationnum {
440 } elsif ( $type eq 'R' ) {
443 #return ($setup, $recur);
453 Returns the customer (L<FS::cust_main> object).
459 my $quotation = $self->quotation or return '';
460 $quotation->cust_main;
465 Returns the prospect (L<FS::prospect_main> object).
471 my $quotation = $self->quotation or return '';
472 $quotation->prospect_main;
478 my @quotation_pkg_without_location =
479 qsearch( 'quotation_pkg', { locationnum => '' } );
480 if (@quotation_pkg_without_location) {
481 warn "setting default location on quotation_pkg records\n";
482 foreach my $quotation_pkg (@quotation_pkg_without_location) {
483 # check() will fix this
484 my $error = $quotation_pkg->replace;
486 die "quotation #".$quotation_pkg->quotationnum.": $error\n";
497 Doesn't support fees, or add-on packages.
501 L<FS::Record>, schema.html from the base documentation.