2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
8 use FS::UID qw( dbh myconnect );
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearch qsearchs );
14 use FS::quotation_pkg;
15 use FS::quotation_pkg_tax;
24 FS::quotation - Object methods for quotation records
30 $record = new FS::quotation \%hash;
31 $record = new FS::quotation { 'column' => 'value' };
33 $error = $record->insert;
35 $error = $new_record->replace($old_record);
37 $error = $record->delete;
39 $error = $record->check;
43 An FS::quotation object represents a quotation. FS::quotation inherits from
44 FS::Record. The following fields are currently supported:
74 projected date when the quotation will be closed
78 projected confidence (expressed as integer) that quotation will close
88 Creates a new quotation. To add the quotation to the database, see L<"insert">.
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to. You can ask the object for a copy with the I<hash> method.
95 sub table { 'quotation'; }
96 sub notice_name { 'Quotation'; }
97 sub template_conf { 'quotation_'; }
98 sub has_sections { 1; }
102 Adds this record to the database. If there is an error, returns the error,
103 otherwise returns false.
107 Delete this record from the database.
109 =item replace OLD_RECORD
111 Replaces the OLD_RECORD with this one in the database. If there is an error,
112 returns the error, otherwise returns false.
116 Checks all fields to make sure this is a valid quotation. If there is
117 an error, returns the error, otherwise returns false. Called by the insert
126 $self->ut_numbern('quotationnum')
127 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
128 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
129 || $self->ut_numbern('_date')
130 || $self->ut_enum('disabled', [ '', 'Y' ])
131 || $self->ut_numbern('usernum')
132 || $self->ut_numbern('close_date')
133 || $self->ut_numbern('confidence')
135 return $error if $error;
137 $self->_date(time) unless $self->_date;
139 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
141 return 'confidence must be an integer between 1 and 100'
142 if length($self->confidence) && (($self->confidence < 1) || ($self->confidence > 100));
144 return 'prospectnum or custnum must be specified'
145 if ! $self->prospectnum
159 sub cust_bill_pkg { #actually quotation_pkg objects
160 shift->quotation_pkg(@_);
169 sprintf('%.2f', $self->_total('setup') + $self->_total('setup_tax'));
172 =item total_recur [ FREQ ]
178 #=item total_recur [ FREQ ]
179 #my $freq = @_ ? shift : '';
180 sprintf('%.2f', $self->_total('recur') + $self->_total('recur_tax'));
184 my( $self, $method ) = @_;
187 $total += $_->$method() for $self->quotation_pkg;
188 sprintf('%.2f', $total);
194 my $opt = shift || {};
195 if ($opt and !ref($opt)) {
196 die ref($self). '->email called with positional parameters';
199 my $conf = $self->conf;
201 my $from = delete $opt->{from};
203 # this is where we set the From: address
204 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
205 || $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
206 $self->SUPER::email( {
217 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
220 #my $cust_main = $self->cust_main;
221 #my $name = $cust_main->name;
222 #my $name_short = $cust_main->name_short;
223 #my $invoice_number = $self->invnum;
224 #my $invoice_date = $self->_date_pretty;
229 =item cust_or_prosect
233 sub cust_or_prospect {
235 $self->custnum ? $self->cust_main : $self->prospect_main;
238 =item cust_or_prospect_label_link
240 HTML links to either the customer or prospect.
242 Returns a list consisting of two elements. The first is a text label for the
243 link, and the second is the URL.
247 sub cust_or_prospect_label_link {
248 my( $self, $p ) = @_;
250 if ( my $custnum = $self->custnum ) {
251 my $display_custnum = $self->cust_main->display_custnum;
252 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
254 : ';show=quotations';
256 emt("View this customer (#[_1])",$display_custnum) =>
257 "${p}view/cust_main.cgi?custnum=$custnum$target"
259 } elsif ( my $prospectnum = $self->prospectnum ) {
261 emt("View this prospect (#[_1])",$prospectnum) =>
262 "${p}view/prospect_main.html?$prospectnum"
270 sub _items_sections {
273 my $escape = $opt{escape}; # the only one we care about
275 my %show; # package frequency => 1 if there's anything to display
276 my %subtotals = (); # package frequency => subtotal
277 my $disable_total = 0;
278 foreach my $pkg ($self->quotation_pkg) {
280 my $part_pkg = $pkg->part_pkg;
282 my $recur_freq = $part_pkg->freq;
283 $show{$recur_freq} = 1 if $pkg->unitrecur > 0 or $pkg->recur_show_zero;
284 $show{0} = 1 if $pkg->unitsetup > 0 or $pkg->setup_show_zero;
285 ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax;
286 ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax;
288 #this is a shitty hack based on what's in part_pkg/ at the moment
289 # but its good enough for the 99% common case of preventing totals from
290 # displaying for prorate packages
292 if $part_pkg->plan =~ /^(prorate|torrus|agent$)/
293 || $part_pkg->option('recur_method') eq 'prorate'
294 || ( $part_pkg->option('sync_bill_date')
296 && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it
300 my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs };
303 my $no_recurring = 0;
304 foreach my $freq (keys %subtotals) {
306 #next if $subtotals{$freq} == 0;
307 next if !$show{$freq};
310 List::MoreUtils::first_index { $_ eq $freq } @pkg_freq_order;
312 if ( $freq eq '0' ) {
313 if ( scalar(keys(%subtotals)) == 1 ) {
314 # there are no recurring packages
316 $desc = $self->mt('Charges');
318 $desc = $self->mt('Setup Charges');
321 $desc = $self->mt('Recurring Charges') . ' - ' .
322 ucfirst($self->mt(FS::Misc->pkg_freqs->{$freq}))
326 'description' => &$escape($desc),
327 'sort_weight' => $weight,
329 'subtotal' => sprintf('%.2f',$subtotals{$freq}),
333 unless ( $disable_total || $no_recurring ) {
335 $total += $_ for values %subtotals;
337 'description' => 'First payment',
339 'category' => 'Total category', #required but what's it used for?
340 'subtotal' => sprintf('%.2f',$total)
344 return \@sections, [];
347 =item enable_previous
351 sub enable_previous { 0 }
353 =item convert_cust_main [ PARAMS ]
355 If this quotation already belongs to a customer, then returns that customer, as
356 an FS::cust_main object.
358 Otherwise, creates a new customer (FS::cust_main object and record, and
359 associated) based on this quotation's prospect, then orders this quotation's
360 packages as real packages for the customer.
362 If there is an error, returns an error message, otherwise, returns the
363 newly-created FS::cust_main object.
365 Accepts the same params as L</order>.
369 sub convert_cust_main {
371 my $params = shift || {};
373 my $cust_main = $self->cust_main;
374 return $cust_main if $cust_main; #already converted, don't again
376 my $oldAutoCommit = $FS::UID::AutoCommit;
377 local $FS::UID::AutoCommit = 0;
380 $cust_main = $self->prospect_main->convert_cust_main;
381 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
382 $dbh->rollback if $oldAutoCommit;
386 $self->prospectnum('');
387 $self->custnum( $cust_main->custnum );
388 my $error = $self->replace || $self->order(undef,$params);
390 $dbh->rollback if $oldAutoCommit;
394 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
400 =item order [ HASHREF ] [ PARAMS ]
402 This method is for use with quotations which are already associated with a customer.
404 Orders this quotation's packages as real packages for the customer.
406 If there is an error, returns an error message, otherwise returns false.
408 If HASHREF is passed, it will be filled with a hash mapping the
409 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
412 If PARAMS hashref is passed, the following params are accepted:
414 onhold - if true, suspends newly ordered packages
420 my $pkgnum_map = shift || {};
421 my $params = shift || {};
422 my $details_map = {};
424 tie my %all_cust_pkg, 'Tie::RefHash';
425 foreach my $quotation_pkg ($self->quotation_pkg) {
426 my $cust_pkg = FS::cust_pkg->new;
427 $pkgnum_map->{ $quotation_pkg->quotationpkgnum } = $cust_pkg;
429 # details will be copied below, after package is ordered
430 $details_map->{ $quotation_pkg->quotationpkgnum } = [
431 map { $_->copy_on_order ? $_->detail : () } $quotation_pkg->quotation_pkg_detail
434 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
435 $cust_pkg->set( $_, $quotation_pkg->get($_) );
438 # can now have two discounts each (setup and recur)
439 foreach my $pkg_discount ($quotation_pkg->quotation_pkg_discount) {
440 my $field = $pkg_discount->setuprecur . '_discountnum';
441 $cust_pkg->set($field, $pkg_discount->discountnum);
444 $all_cust_pkg{$cust_pkg} = []; # no services
447 local $SIG{HUP} = 'IGNORE';
448 local $SIG{INT} = 'IGNORE';
449 local $SIG{QUIT} = 'IGNORE';
450 local $SIG{TERM} = 'IGNORE';
451 local $SIG{TSTP} = 'IGNORE';
452 local $SIG{PIPE} = 'IGNORE';
454 my $oldAutoCommit = $FS::UID::AutoCommit;
455 local $FS::UID::AutoCommit = 0;
458 my $error = $self->cust_main->order_pkgs( \%all_cust_pkg );
461 # copy details (copy_on_order filtering handled above)
462 foreach my $quotationpkgnum (keys %$details_map) {
463 next unless @{$details_map->{$quotationpkgnum}};
464 $error = $pkgnum_map->{$quotationpkgnum}->set_cust_pkg_detail(
466 @{$details_map->{$quotationpkgnum}}
472 if ($$params{'onhold'}) {
473 foreach my $quotationpkgnum (keys %$pkgnum_map) {
475 $error = $pkgnum_map->{$quotationpkgnum}->suspend();
480 $dbh->rollback if $oldAutoCommit;
484 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
486 foreach my $quotationpkgnum (keys %$pkgnum_map) {
487 # convert the objects to just pkgnums
488 my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
489 $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
498 One-time charges, like FS::cust_main::charge()
502 #super false laziness w/cust_main::charge
505 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
506 my ( $pkg, $comment, $additional );
507 my ( $setuptax, $taxclass ); #internal taxes
508 my ( $taxproduct, $override ); #vendor (CCH) taxes
510 my $cust_pkg_ref = '';
511 my ( $bill_now, $invoice_terms ) = ( 0, '' );
513 my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
514 if ( ref( $_[0] ) ) {
515 $amount = $_[0]->{amount};
516 $setup_cost = $_[0]->{setup_cost};
517 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
518 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
519 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
520 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
521 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
522 : '$'. sprintf("%.2f",$amount);
523 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
524 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
525 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
526 $additional = $_[0]->{additional} || [];
527 $taxproduct = $_[0]->{taxproductnum};
528 $override = { '' => $_[0]->{tax_override} };
529 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
530 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
531 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
532 $locationnum = $_[0]->{locationnum};
533 $discountnum = $_->{setup_discountnum};
534 $discountnum_amount = $_->{setup_discountnum_amount};
535 $discountnum_percent = $_->{setup_discountnum_percent};
541 $pkg = @_ ? shift : 'One-time charge';
542 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
544 $taxclass = @_ ? shift : '';
548 local $SIG{HUP} = 'IGNORE';
549 local $SIG{INT} = 'IGNORE';
550 local $SIG{QUIT} = 'IGNORE';
551 local $SIG{TERM} = 'IGNORE';
552 local $SIG{TSTP} = 'IGNORE';
553 local $SIG{PIPE} = 'IGNORE';
555 my $oldAutoCommit = $FS::UID::AutoCommit;
556 local $FS::UID::AutoCommit = 0;
559 my $part_pkg = new FS::part_pkg ( {
561 'comment' => $comment,
565 'classnum' => ( $classnum ? $classnum : '' ),
566 'setuptax' => $setuptax,
567 'taxclass' => $taxclass,
568 'taxproductnum' => $taxproduct,
569 'setup_cost' => $setup_cost,
572 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
573 ( 0 .. @$additional - 1 )
575 'additional_count' => scalar(@$additional),
576 'setup_fee' => $amount,
579 my $error = $part_pkg->insert( options => \%options,
580 tax_overrides => $override,
583 $dbh->rollback if $oldAutoCommit;
587 my $pkgpart = $part_pkg->pkgpart;
590 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
592 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
593 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
594 $error = $type_pkgs->insert;
596 $dbh->rollback if $oldAutoCommit;
601 #except for DIFF, eveything above is idential to cust_main version
602 #but below is our own thing pretty much (adding a quotation package instead
603 # of ordering a customer package, no "bill now")
605 my $quotation_pkg = new FS::quotation_pkg ( {
606 'quotationnum' => $self->quotationnum,
607 'pkgpart' => $pkgpart,
608 'quantity' => $quantity,
609 #'start_date' => $start_date,
610 #'no_auto' => $no_auto,
611 'locationnum' => $locationnum,
612 'setup_discountnum' => $discountnum,
613 'setup_discountnum_amount' => $discountnum_amount,
614 'setup_discountnum_percent' => $discountnum_percent,
617 $error = $quotation_pkg->insert;
619 $dbh->rollback if $oldAutoCommit;
621 #} elsif ( $cust_pkg_ref ) {
622 # ${$cust_pkg_ref} = $cust_pkg;
625 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
632 Disables this quotation (sets disabled to Y, which hides the quotation on
633 prospects and customers).
635 If there is an error, returns an error message, otherwise returns false.
641 $self->disabled('Y');
647 Enables this quotation.
649 If there is an error, returns an error message, otherwise returns false.
661 Calculates current prices for all items on this quotation, including
662 discounts and taxes, and updates the quotation_pkg records accordingly.
668 my $conf = FS::Conf->new;
670 my %pkgnum_of; # quotationpkgnum => temporary pkgnum
672 my $me = "[quotation #".$self->quotationnum."]"; # for debug messages
674 my @return_bill = ([]);
677 ###### BEGIN TRANSACTION ######
681 my $temp_dbh = myconnect();
682 local $FS::UID::dbh = $temp_dbh;
683 local $FS::UID::AutoCommit = 0;
685 my $fake_self = FS::quotation->new({ $self->hash });
687 # if this is a prospect, make them into a customer for now
688 # XXX prospects currently can't have service locations
689 my $cust_or_prospect = $self->cust_or_prospect;
691 if ( $cust_or_prospect->isa('FS::prospect_main') ) {
692 $cust_main = $cust_or_prospect->convert_cust_main;
693 die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
694 $fake_self->set('prospectnum', '');
695 $fake_self->set('custnum', $cust_main->custnum);
697 $cust_main = $cust_or_prospect;
701 $error = $fake_self->order(\%pkgnum_of);
702 die "$error (simulating package order)\n" if $error;
704 my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
706 # simulate the first bill
709 'pkg_list' => \@new_pkgs,
710 'time' => time, # an option to adjust this?
711 'return_bill' => $return_bill[0],
712 'no_usage_reset' => 1,
714 $error = $cust_main->bill(%bill_opt);
715 die "$error (simulating initial billing)\n" if $error;
717 # pick dates for future bills
719 foreach (@new_pkgs) {
720 my $bill = $_->get('bill');
722 push @{ $next_bill_pkgs{$bill} ||= [] }, $_;
726 foreach my $next_bill (keys %next_bill_pkgs) {
727 $bill_opt{'time'} = $next_bill;
728 $bill_opt{'return_bill'} = $return_bill[$i] = [];
729 $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
730 $error = $cust_main->bill(%bill_opt);
731 die "$error (simulating recurring billing cycle $i)\n" if $error;
738 ###### END TRANSACTION ######
739 my %quotationpkgnum_of = reverse %pkgnum_of;
742 warn "pkgnums:\n".Dumper(\%pkgnum_of);
743 warn Dumper(\@return_bill);
746 # Careful: none of the foreign keys in here are correct outside the sandbox.
747 # We have a translation table for pkgnums; all others are total lies.
749 my %quotation_pkg; # quotationpkgnum => quotation_pkg
750 foreach my $qp ($self->quotation_pkg) {
751 $quotation_pkg{$qp->quotationpkgnum} = $qp;
752 $qp->set($_, 0) foreach qw(unitsetup unitrecur);
753 $qp->set('freq', '');
754 # flush old tax records
755 foreach ($qp->quotation_pkg_tax) {
757 return "$error (flushing tax records for pkgpart ".$qp->part_pkg->pkgpart.")"
762 my %quotation_pkg_tax; # quotationpkgnum => tax name => quotation_pkg_tax obj
763 my %quotation_pkg_discount; # quotationpkgnum => setuprecur => quotation_pkg_discount obj
765 for (my $i = 0; $i < scalar(@return_bill); $i++) {
766 my $this_bill = $return_bill[$i]->[0];
768 warn "$me billing cycle $i produced no invoice\n";
774 foreach my $cust_bill_pkg (@{ $this_bill->get('cust_bill_pkg') }) {
775 my $pkgnum = $cust_bill_pkg->pkgnum;
776 $cust_bill_pkg{ $cust_bill_pkg->billpkgnum } = $cust_bill_pkg;
778 # taxes/fees; come back to it
779 push @nonpkg_lines, $cust_bill_pkg;
782 my $quotationpkgnum = $quotationpkgnum_of{$pkgnum};
783 my $qp = $quotation_pkg{$quotationpkgnum};
785 # XXX supplemental packages could do this (they have separate pkgnums)
786 # handle that special case at some point
787 warn "$me simulated bill returned a package not on the quotation (pkgpart ".$cust_bill_pkg->pkgpart.")\n";
791 # then this is the first (setup) invoice
792 $qp->set('start_date', $cust_bill_pkg->sdate);
793 $qp->set('unitsetup', $qp->unitsetup + $cust_bill_pkg->unitsetup);
794 # pkgpart_override is a possibility
796 # recurring invoice (should be only one of these per package, though
797 # it may have multiple lineitems with the same pkgnum)
798 $qp->set('unitrecur', $qp->unitrecur + $cust_bill_pkg->unitrecur);
802 if ( $cust_bill_pkg->get('discounts') ) {
803 # discount records are generated as (setup, recur).
804 # well, not always, sometimes it's just (recur), but fixing this
805 # is horribly invasive.
806 my $discount = $cust_bill_pkg->get('discounts')->[0];
809 # find the quotation_pkg_discount record for this billing pass...
810 my $setuprecur = $i ? 'recur' : 'setup';
811 my $qpd = $quotation_pkg_discount{$quotationpkgnum}{$setuprecur}
812 ||= qsearchs('quotation_pkg_discount', {
813 'quotationpkgnum' => $quotationpkgnum,
814 'setuprecur' => $setuprecur,
817 if (!$qpd) { #can't happen
818 warn "$me simulated bill returned a $setuprecur discount but no discount is in effect.\n";
821 $qpd->set('amount', $discount->amount);
824 } # end of discount stuff
829 foreach my $cust_bill_pkg (@nonpkg_lines) {
831 my $itemdesc = $cust_bill_pkg->itemdesc;
833 if ($cust_bill_pkg->feepart) {
834 warn "$me simulated bill included a non-package fee (feepart ".
835 $cust_bill_pkg->feepart.")\n";
838 my $links = $cust_bill_pkg->get('cust_bill_pkg_tax_location') ||
839 $cust_bill_pkg->get('cust_bill_pkg_tax_rate_location') ||
841 # breadth-first unrolled recursion:
842 # take each tax link and any tax-on-tax descendants, and merge them
843 # into a single quotation_pkg_tax record for each pkgnum/taxname
845 while (my $tax_link = shift @$links) {
846 my $target = $cust_bill_pkg{ $tax_link->taxable_billpkgnum }
847 or die "$me unable to resolve tax link\n";
848 if ($target->pkgnum) {
849 my $quotationpkgnum = $quotationpkgnum_of{$target->pkgnum};
850 # create this if there isn't one yet
851 my $qpt = $quotation_pkg_tax{$quotationpkgnum}{$itemdesc} ||=
852 FS::quotation_pkg_tax->new({
853 quotationpkgnum => $quotationpkgnum,
854 itemdesc => $itemdesc,
858 if ( $i == 0 ) { # first invoice
859 $qpt->set('setup_amount', $qpt->setup_amount + $tax_link->amount);
860 } else { # subsequent invoices
861 # this isn't perfectly accurate, but that's why it's an estimate
862 $qpt->set('recur_amount', $qpt->recur_amount + $tax_link->amount);
863 $qpt->set('setup_amount', sprintf('%.2f', $qpt->setup_amount - $tax_link->amount));
864 $qpt->set('setup_amount', 0) if $qpt->setup_amount < 0;
866 } elsif ($target->feepart) {
867 # do nothing; we already warned for the fee itself
869 # tax on tax: the tax target is another tax item.
870 # since this is an estimate, I'm just going to assign it to the
871 # first of the underlying packages. (RT#5243 is why we can't have
873 my $sublinks = $target->cust_bill_pkg_tax_rate_location;
874 if ($sublinks and $sublinks->[0]) {
875 $tax_link->set('taxable_billpkgnum', $sublinks->[0]->taxable_billpkgnum);
876 push @$links, $tax_link; #try again
878 warn "$me unable to assign tax on tax; ignoring\n";
881 } # while my $tax_link
883 } # foreach my $cust_bill_pkg
885 foreach my $quotation_pkg (values %quotation_pkg) {
886 $error = $quotation_pkg->replace;
887 return "$error (recording estimate for ".$quotation_pkg->part_pkg->pkg.")"
890 foreach (values %quotation_pkg_discount) {
891 # { setup => one, recur => another }
892 foreach my $quotation_pkg_discount (values %$_) {
893 $error = $quotation_pkg_discount->replace;
894 return "$error (recording estimated discount)"
898 foreach my $quotation_pkg_tax (map { values %$_ } values %quotation_pkg_tax) {
899 $error = $quotation_pkg_tax->insert;
900 return "$error (recording estimated tax for ".$quotation_pkg_tax->itemdesc.")"
913 =item search_sql_where HASHREF
915 Class method which returns an SQL WHERE fragment to search for parameters
916 specified in HASHREF. Valid parameters are
922 List reference of start date, end date, as UNIX timestamps.
932 List reference of charged limits (exclusive).
936 List reference of charged limits (exclusive).
940 flag, return open invoices only
944 flag, return net invoices only
952 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
956 sub search_sql_where {
957 my($class, $param) = @_;
959 # warn "$me search_sql_where called with params: \n".
960 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
966 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
967 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
971 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
972 # push @search, "cust_main.refnum = $1";
976 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
977 push @search, "quotation.prospectnum = $1";
981 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
982 push @search, "cust_bill.custnum = $1";
986 if ( $param->{_date} ) {
987 my($beginning, $ending) = @{$param->{_date}};
989 push @search, "quotation._date >= $beginning",
990 "quotation._date < $ending";
994 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
995 push @search, "quotation.quotationnum >= $1";
997 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
998 push @search, "quotation.quotationnum <= $1";
1002 # if ( $param->{charged} ) {
1003 # my @charged = ref($param->{charged})
1004 # ? @{ $param->{charged} }
1005 # : ($param->{charged});
1007 # push @search, map { s/^charged/cust_bill.charged/; $_; }
1011 my $owed_sql = FS::cust_bill->owed_sql;
1014 push @search, "quotation._date < ". (time-86400*$param->{'days'})
1015 if $param->{'days'};
1017 #agent virtualization
1018 my $curuser = $FS::CurrentUser::CurrentUser;
1019 #false laziness w/search/quotation.html
1020 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
1021 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
1024 join(' AND ', @search );
1030 Return line item hashes for each package on this quotation.
1035 my ($self, %options) = @_;
1036 my $escape = $options{'escape_function'};
1037 my $locale = $self->cust_or_prospect->locale;
1039 my $preref = $options{'preref_callback'};
1041 my $section = $options{'section'};
1042 my $freq = $section->{'category'};
1043 my @pkgs = $self->quotation_pkg;
1045 die "_items_pkg called without section->{'category'}"
1046 unless defined $freq;
1048 my %tax_item; # taxname => hashref, will be aggregated AT DISPLAY TIME
1049 # like we should have done in the first place
1051 foreach my $quotation_pkg (@pkgs) {
1052 my $part_pkg = $quotation_pkg->part_pkg;
1053 my @details = $quotation_pkg->details;
1056 'pkgnum' => $quotation_pkg->quotationpkgnum,
1057 'description' => $quotation_pkg->desc($locale),
1058 'ext_description' => \@details,
1059 'quantity' => $quotation_pkg->quantity,
1063 $setuprecur = 'setup';
1064 if ($part_pkg->freq ne '0') {
1065 # indicate that it's a setup fee on a recur package (cust_bill does
1067 $this_item->{'description'} .= ' Setup';
1070 # recur for this frequency
1071 next if $freq ne $part_pkg->freq;
1072 $setuprecur = 'recur';
1075 $this_item->{'unit_amount'} = sprintf('%.2f',
1076 $quotation_pkg->get('unit'.$setuprecur));
1077 $this_item->{'amount'} = sprintf('%.2f', $this_item->{'unit_amount'}
1078 * $quotation_pkg->quantity);
1079 next if $this_item->{'amount'} == 0 and !(
1080 $setuprecur eq 'setup'
1081 ? $quotation_pkg->setup_show_zero
1082 : $quotation_pkg->recur_show_zero
1086 $this_item->{'preref_html'} = &$preref($quotation_pkg);
1089 push @items, $this_item;
1090 my $discount = $quotation_pkg->_item_discount(setuprecur => $setuprecur);
1092 $_ = &{$escape}($_) foreach @{ $discount->{ext_description} };
1093 push @items, $discount;
1096 # each quotation_pkg_tax has two amounts: the amount charged on the
1097 # setup invoice, and the amount on the recurring invoice.
1098 foreach my $qpt ($quotation_pkg->quotation_pkg_tax) {
1099 my $this_tax = $tax_item{$qpt->itemdesc} ||= {
1101 'description' => $qpt->itemdesc,
1102 'ext_description' => [],
1105 $this_tax->{'amount'} += $qpt->get($setuprecur.'_amount');
1107 } # foreach $quotation_pkg
1109 foreach my $taxname ( sort { $a cmp $b } keys (%tax_item) ) {
1110 my $this_tax = $tax_item{$taxname};
1111 $this_tax->{'amount'} = sprintf('%.2f', $this_tax->{'amount'});
1112 next if $this_tax->{'amount'} == 0;
1113 push @items, $this_tax;
1129 L<FS::Record>, schema.html from the base documentation.