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 List::Util qw(sum);
14 FS::quotation_pkg - Object methods for quotation_pkg records
18 use FS::quotation_pkg;
20 $record = new FS::quotation_pkg \%hash;
21 $record = new FS::quotation_pkg { 'column' => 'value' };
23 $error = $record->insert;
25 $error = $new_record->replace($old_record);
27 $error = $record->delete;
29 $error = $record->check;
33 An FS::quotation_pkg object represents a quotation package.
34 FS::quotation_pkg inherits from FS::Record. The following fields are currently
45 pkgpart (L<FS::part_pkg>) of the package
49 locationnum (L<FS::cust_location>) where the package will be in service
53 expected start date for the package, as a timestamp
65 'Y' to waive the setup fee
69 The amount per package that will be charged in setup/one-time fees.
73 The amount per package that will be charged per billing cycle.
83 Creates a new quotation package. To add the quotation package to the database,
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to. You can ask the object for a copy with the I<hash> method.
91 sub table { 'quotation_pkg'; }
93 sub display_table { 'quotation_pkg'; }
95 #forget it, just overriding cust_bill_pkg_display entirely
96 #sub display_table_orderby { 'quotationpkgnum'; } # something else?
97 # # (for invoice display order)
99 sub discount_table { 'quotation_pkg_discount'; }
103 Adds this record to the database. If there is an error, returns the error,
104 otherwise returns false.
109 my ($self, %options) = @_;
112 my $oldAutoCommit = $FS::UID::AutoCommit;
113 local $FS::UID::AutoCommit = 0;
115 my $error = $self->SUPER::insert;
117 if ( !$error and $self->discountnum ) {
118 $error = $self->insert_discount;
119 $error .= ' (setting discount)' if $error;
122 # update $self and any discounts with their amounts
124 $error = $self->estimate;
125 $error .= ' (calculating charges)' if $error;
129 $dbh->rollback if $oldAutoCommit;
132 $dbh->commit if $oldAutoCommit;
139 Delete this record from the database.
147 my $oldAutoCommit = $FS::UID::AutoCommit;
148 local $FS::UID::AutoCommit = 0;
150 foreach ($self->quotation_pkg_discount) {
151 my $error = $_->delete;
153 $dbh->rollback if $oldAutoCommit;
154 return $error . ' (deleting discount)';
158 my $error = $self->SUPER::delete;
160 $dbh->rollback if $oldAutoCommit;
163 $dbh->commit if $oldAutoCommit;
169 =item replace OLD_RECORD
171 Replaces the OLD_RECORD with this one in the database. If there is an error,
172 returns the error, otherwise returns false.
176 Checks all fields to make sure this is a valid quotation package. If there is
177 an error, returns the error, otherwise returns false. Called by the insert
186 $self->ut_numbern('quotationpkgnum')
187 || $self->ut_foreign_key( 'quotationnum', 'quotation', 'quotationnum' )
188 || $self->ut_foreign_key( 'pkgpart', 'part_pkg', 'pkgpart' )
189 || $self->ut_foreign_keyn( 'locationnum', 'cust_location', 'locationnum' )
190 || $self->ut_numbern('start_date')
191 || $self->ut_numbern('contract_end')
192 || $self->ut_numbern('quantity')
193 || $self->ut_moneyn('unitsetup')
194 || $self->ut_moneyn('unitrecur')
195 || $self->ut_enum('waive_setup', [ '', 'Y'] )
198 return $error if $error;
205 qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart } );
210 $self->part_pkg->pkg;
215 Update the quotation_pkg record with the estimated setup and recurring
216 charges for the package. Returns nothing on success, or an error message
223 my $part_pkg = $self->part_pkg;
224 my $quantity = $self->quantity || 1;
225 my ($unitsetup, $unitrecur);
226 # calculate base fees
227 if ( $self->waive_setup eq 'Y' || $self->{'_NO_SETUP_KLUDGE'} ) {
230 $unitsetup = $part_pkg->option('setup_fee',1) || '0.00'; # XXX 3.x only
232 if ( $self->{'_NO_RECUR_KLUDGE'} ) {
235 $unitrecur = $part_pkg->base_recur;
240 $self->set('unitsetup', $unitsetup);
241 $self->set('unitrecur', $unitrecur);
242 my $error = $self->replace;
243 return $error if $error;
245 # semi-duplicates calc_discount
246 my $setup_discount = 0;
247 my $recur_discount = 0;
249 my %setup_discounts; # quotationpkgdiscountnum => amount
250 my %recur_discounts; # quotationpkgdiscountnum => amount
252 # XXX the order of applying discounts is ill-defined, which matters
253 # if there are percentage and amount discounts on the same package.
255 # but right now there can only be one discount on any package, so
257 foreach my $pkg_discount ($self->quotation_pkg_discount) {
259 my $discount = $pkg_discount->discount;
260 my $this_setup_discount = 0;
261 my $this_recur_discount = 0;
263 if ( $discount->percent > 0 ) {
265 if ( $discount->setup ) {
266 $this_setup_discount = ($discount->percent * $unitsetup / 100);
268 $this_recur_discount = ($discount->percent * $unitrecur / 100);
270 } elsif ( $discount->amount > 0 ) {
272 my $discount_left = $discount->amount;
273 if ( $discount->setup ) {
274 if ( $discount_left > $unitsetup - $setup_discount ) {
275 # then discount the setup to zero
276 $discount_left -= $unitsetup - $setup_discount;
277 $this_setup_discount = $unitsetup - $setup_discount;
279 # not enough discount to fully cover the setup
280 $this_setup_discount = $discount_left;
284 # same logic for recur
285 if ( $discount_left > $unitrecur - $recur_discount ) {
286 $this_recur_discount = $unitrecur - $recur_discount;
288 $this_recur_discount = $discount_left;
293 # increment the total discountage
294 $setup_discount += $this_setup_discount;
295 $recur_discount += $this_recur_discount;
296 # and update the pkg_discount object
297 $pkg_discount->set('setup_amount', sprintf('%.2f', $setup_discount));
298 $pkg_discount->set('recur_amount', sprintf('%.2f', $recur_discount));
299 my $error = $pkg_discount->replace;
300 return $error if $error;
306 =item insert_discount
308 Associates this package with a discount (see L<FS::cust_pkg_discount>,
309 possibly inserting a new discount on the fly (see L<FS::discount>). Properties
310 of the discount will be taken from this object.
314 sub insert_discount {
315 #my ($self, %options) = @_;
318 my $quotation_pkg_discount = FS::quotation_pkg_discount->new( {
319 'quotationpkgnum' => $self->quotationpkgnum,
320 'discountnum' => $self->discountnum,
321 #for the create a new discount case
322 '_type' => $self->discountnum__type,
323 'amount' => $self->discountnum_amount,
324 'percent' => $self->discountnum_percent,
325 'months' => $self->discountnum_months,
326 'setup' => $self->discountnum_setup,
329 $quotation_pkg_discount->insert;
334 my @pkg_discounts = $self->pkg_discount;
335 return if @pkg_discounts == 0;
340 description => $self->mt('Discount'),
344 ext_description => \@ext,
345 # maybe should show quantity/unit discount?
347 foreach my $pkg_discount (@pkg_discounts) {
348 push @ext, $pkg_discount->description;
349 $d->{setup_amount} -= $pkg_discount->setup_amount;
350 $d->{recur_amount} -= $pkg_discount->recur_amount;
352 $d->{setup_amount} *= $self->quantity || 1;
353 $d->{recur_amount} *= $self->quantity || 1;
354 $d->{amount} = $d->{setup_amount} + $d->{recur_amount};
361 ($self->unitsetup - sum(map { $_->setup_amount } $self->pkg_discount))
362 * ($self->quantity || 1);
367 ($self->unitrecur - sum(map { $_->recur_amount } $self->pkg_discount))
368 * ($self->quantity || 1);
371 =item cust_bill_pkg_display [ type => TYPE ]
375 sub cust_bill_pkg_display {
376 my ( $self, %opt ) = @_;
378 my $type = $opt{type} if exists $opt{type};
379 return () if $type eq 'U'; #quotations don't have usage
381 if ( $self->get('display') ) {
382 return ( grep { defined($type) ? ($type eq $_->type) : 1 }
383 @{ $self->get('display') }
388 my $setup = $self->new($self->hashref);
389 $setup->{'_NO_RECUR_KLUDGE'} = 1;
390 $setup->{'type'} = 'S';
391 my $recur = $self->new($self->hashref);
392 $recur->{'_NO_SETUP_KLUDGE'} = 1;
393 $recur->{'type'} = 'R';
395 if ( $type eq 'S' ) {
397 } elsif ( $type eq 'R' ) {
400 #return ($setup, $recur);
410 Returns the customer (L<FS::cust_main> object).
416 my $quotation = FS::quotation->by_key($self->quotationnum) or return '';
417 $quotation->cust_main;
424 FS::quotation->by_key($self->quotationnum);
427 sub quotation_pkg_discount {
429 qsearch('quotation_pkg_discount', { quotationpkgnum => $self->quotationpkgnum });
436 Doesn't support taxes, fees, or add-on packages.
440 L<FS::Record>, schema.html from the base documentation.