2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearchs );
13 use FS::quotation_pkg;
18 FS::quotation - Object methods for quotation records
24 $record = new FS::quotation \%hash;
25 $record = new FS::quotation { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::quotation object represents a quotation. FS::quotation inherits from
38 FS::Record. The following fields are currently supported:
75 Creates a new quotation. To add the quotation to the database, see L<"insert">.
77 Note that this stores the hash reference, not a distinct copy of the hash it
78 points to. You can ask the object for a copy with the I<hash> method.
82 sub table { 'quotation'; }
83 sub notice_name { 'Quotation'; }
84 sub template_conf { 'quotation_'; }
88 Adds this record to the database. If there is an error, returns the error,
89 otherwise returns false.
93 Delete this record from the database.
95 =item replace OLD_RECORD
97 Replaces the OLD_RECORD with this one in the database. If there is an error,
98 returns the error, otherwise returns false.
102 Checks all fields to make sure this is a valid quotation. If there is
103 an error, returns the error, otherwise returns false. Called by the insert
112 $self->ut_numbern('quotationnum')
113 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
114 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
115 || $self->ut_numbern('_date')
116 || $self->ut_enum('disabled', [ '', 'Y' ])
117 || $self->ut_numbern('usernum')
119 return $error if $error;
121 $self->_date(time) unless $self->_date;
123 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
125 return 'prospectnum or custnum must be specified'
126 if ! $self->prospectnum
140 sub cust_bill_pkg { #actually quotation_pkg objects
141 shift->quotation_pkg(@_);
150 $self->_total('setup');
153 =item total_recur [ FREQ ]
159 #=item total_recur [ FREQ ]
160 #my $freq = @_ ? shift : '';
161 $self->_total('recur');
165 my( $self, $method ) = @_;
168 $total += $_->$method() for $self->cust_bill_pkg;
169 sprintf('%.2f', $total);
175 my $opt = shift || {};
176 if ($opt and !ref($opt)) {
177 die ref($self). '->email called with positional parameters';
180 my $conf = $self->conf;
182 my $from = delete $opt->{from};
184 # this is where we set the From: address
185 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
186 || $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
187 $self->SUPER::email( {
198 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
201 #my $cust_main = $self->cust_main;
202 #my $name = $cust_main->name;
203 #my $name_short = $cust_main->name_short;
204 #my $invoice_number = $self->invnum;
205 #my $invoice_date = $self->_date_pretty;
210 =item cust_or_prosect
214 sub cust_or_prospect {
216 $self->custnum ? $self->cust_main : $self->prospect_main;
219 =item cust_or_prospect_label_link P
221 HTML links to either the customer or prospect.
223 Returns a list consisting of two elements. The first is a text label for the
224 link, and the second is the URL.
228 sub cust_or_prospect_label_link {
229 my( $self, $p ) = @_;
231 if ( my $custnum = $self->custnum ) {
232 my $display_custnum = $self->cust_main->display_custnum;
233 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
235 : ';show=quotations';
237 emt("View this customer (#[_1])",$display_custnum) =>
238 "${p}view/cust_main.cgi?custnum=$custnum$target"
240 } elsif ( my $prospectnum = $self->prospectnum ) {
242 emt("View this prospect (#[_1])",$prospectnum) =>
243 "${p}view/prospect_main.html?$prospectnum"
251 #prevent things from falsely showing up as taxes, at least until we support
252 # quoting tax amounts..
257 shift->cust_bill_pkg;
261 my( $self, $total_items ) = @_;
263 if ( $self->total_setup > 0 ) {
264 push @$total_items, {
265 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
266 'total_amount' => $self->total_setup,
270 #could/should add up the different recurring frequencies on lines of their own
271 # but this will cover the 95% cases for now
272 if ( $self->total_recur > 0 ) {
273 push @$total_items, {
274 'total_item' => $self->mt('Total Recurring'),
275 'total_amount' => $self->total_recur,
281 =item enable_previous
285 sub enable_previous { 0 }
287 =item convert_cust_main
289 If this quotation already belongs to a customer, then returns that customer, as
290 an FS::cust_main object.
292 Otherwise, creates a new customer (FS::cust_main object and record, and
293 associated) based on this quotation's prospect, then orders this quotation's
294 packages as real packages for the customer.
296 If there is an error, returns an error message, otherwise, returns the
297 newly-created FS::cust_main object.
301 sub convert_cust_main {
304 my $cust_main = $self->cust_main;
305 return $cust_main if $cust_main; #already converted, don't again
307 my $oldAutoCommit = $FS::UID::AutoCommit;
308 local $FS::UID::AutoCommit = 0;
311 $cust_main = $self->prospect_main->convert_cust_main;
312 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
313 $dbh->rollback if $oldAutoCommit;
317 $self->prospectnum('');
318 $self->custnum( $cust_main->custnum );
319 my $error = $self->replace || $self->order;
321 $dbh->rollback if $oldAutoCommit;
325 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
333 This method is for use with quotations which are already associated with a customer.
335 Orders this quotation's packages as real packages for the customer.
337 If there is an error, returns an error message, otherwise returns false.
344 tie my %all_cust_pkg, 'Tie::RefHash';
345 foreach my $quotation_pkg ($self->quotation_pkg) {
346 my $cust_pkg = FS::cust_pkg->new;
347 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
348 $cust_pkg->set( $_, $quotation_pkg->get($_) );
351 # currently only one discount each
352 my ($pkg_discount) = $quotation_pkg->quotation_pkg_discount;
353 if ( $pkg_discount ) {
354 $cust_pkg->set('discountnum', $pkg_discount->discountnum);
357 $all_cust_pkg{$cust_pkg} = []; # no services
360 $self->cust_main->order_pkgs( \%all_cust_pkg );
366 One-time charges, like FS::cust_main::charge()
370 #super false laziness w/cust_main::charge
373 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
374 my ( $pkg, $comment, $additional );
375 my ( $setuptax, $taxclass ); #internal taxes
376 my ( $taxproduct, $override ); #vendor (CCH) taxes
378 my $cust_pkg_ref = '';
379 my ( $bill_now, $invoice_terms ) = ( 0, '' );
381 if ( ref( $_[0] ) ) {
382 $amount = $_[0]->{amount};
383 $setup_cost = $_[0]->{setup_cost};
384 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
385 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
386 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
387 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
388 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
389 : '$'. sprintf("%.2f",$amount);
390 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
391 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
392 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
393 $additional = $_[0]->{additional} || [];
394 $taxproduct = $_[0]->{taxproductnum};
395 $override = { '' => $_[0]->{tax_override} };
396 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
397 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
398 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
399 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
405 $pkg = @_ ? shift : 'One-time charge';
406 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
408 $taxclass = @_ ? shift : '';
412 local $SIG{HUP} = 'IGNORE';
413 local $SIG{INT} = 'IGNORE';
414 local $SIG{QUIT} = 'IGNORE';
415 local $SIG{TERM} = 'IGNORE';
416 local $SIG{TSTP} = 'IGNORE';
417 local $SIG{PIPE} = 'IGNORE';
419 my $oldAutoCommit = $FS::UID::AutoCommit;
420 local $FS::UID::AutoCommit = 0;
423 my $part_pkg = new FS::part_pkg ( {
425 'comment' => $comment,
429 'classnum' => ( $classnum ? $classnum : '' ),
430 'setuptax' => $setuptax,
431 'taxclass' => $taxclass,
432 'taxproductnum' => $taxproduct,
433 'setup_cost' => $setup_cost,
436 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
437 ( 0 .. @$additional - 1 )
439 'additional_count' => scalar(@$additional),
440 'setup_fee' => $amount,
443 my $error = $part_pkg->insert( options => \%options,
444 tax_overrides => $override,
447 $dbh->rollback if $oldAutoCommit;
451 my $pkgpart = $part_pkg->pkgpart;
454 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
456 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
457 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
458 $error = $type_pkgs->insert;
460 $dbh->rollback if $oldAutoCommit;
465 #except for DIFF, eveything above is idential to cust_main version
466 #but below is our own thing pretty much (adding a quotation package instead
467 # of ordering a customer package, no "bill now")
469 my $quotation_pkg = new FS::quotation_pkg ( {
470 'quotationnum' => $self->quotationnum,
471 'pkgpart' => $pkgpart,
472 'quantity' => $quantity,
473 #'start_date' => $start_date,
474 #'no_auto' => $no_auto,
475 'locationnum'=> $locationnum,
478 $error = $quotation_pkg->insert;
480 $dbh->rollback if $oldAutoCommit;
482 #} elsif ( $cust_pkg_ref ) {
483 # ${$cust_pkg_ref} = $cust_pkg;
486 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
493 Disables this quotation (sets disabled to Y, which hides the quotation on
494 prospects and customers).
496 If there is an error, returns an error message, otherwise returns false.
502 $self->disabled('Y');
508 Enables this quotation.
510 If there is an error, returns an error message, otherwise returns false.
527 =item search_sql_where HASHREF
529 Class method which returns an SQL WHERE fragment to search for parameters
530 specified in HASHREF. Valid parameters are
536 List reference of start date, end date, as UNIX timestamps.
546 List reference of charged limits (exclusive).
550 List reference of charged limits (exclusive).
554 flag, return open invoices only
558 flag, return net invoices only
566 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
570 sub search_sql_where {
571 my($class, $param) = @_;
573 # warn "$me search_sql_where called with params: \n".
574 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
580 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
581 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
585 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
586 # push @search, "cust_main.refnum = $1";
590 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
591 push @search, "quotation.prospectnum = $1";
595 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
596 push @search, "cust_bill.custnum = $1";
600 if ( $param->{_date} ) {
601 my($beginning, $ending) = @{$param->{_date}};
603 push @search, "quotation._date >= $beginning",
604 "quotation._date < $ending";
608 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
609 push @search, "quotation.quotationnum >= $1";
611 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
612 push @search, "quotation.quotationnum <= $1";
616 # if ( $param->{charged} ) {
617 # my @charged = ref($param->{charged})
618 # ? @{ $param->{charged} }
619 # : ($param->{charged});
621 # push @search, map { s/^charged/cust_bill.charged/; $_; }
625 my $owed_sql = FS::cust_bill->owed_sql;
628 push @search, "quotation._date < ". (time-86400*$param->{'days'})
631 #agent virtualization
632 my $curuser = $FS::CurrentUser::CurrentUser;
633 #false laziness w/search/quotation.html
634 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
635 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
638 join(' AND ', @search );
644 Return line item hashes for each package on this quotation. Differs from the
645 base L<FS::Template_Mixin> version in that it recalculates each quoted package
646 first, and doesn't implement the "condensed" option.
651 my ($self, %options) = @_;
652 my @quotation_pkg = $self->quotation_pkg;
653 foreach (@quotation_pkg) {
654 my $error = $_->estimate;
655 die "error calculating estimate for pkgpart " . $_->pkgpart.": $error\n"
659 # run it through the Template_Mixin engine
660 return $self->_items_cust_bill_pkg(\@quotation_pkg, %options);
669 L<FS::Record>, schema.html from the base documentation.