From: Mark Wells Date: Sat, 25 Apr 2015 22:02:15 +0000 (-0700) Subject: selfservice quotations, #33852 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=c110da0da864245e47cae019b8a347367cc6430c selfservice quotations, #33852 --- diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm index 695b4cab3..b6f8aa4c6 100644 --- a/FS/FS/ClientAPI/MasonComponent.pm +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -27,6 +27,7 @@ my %allowed_comps = map { $_=>1 } qw( my %session_comps = map { $_=>1 } qw( /elements/location.html /elements/tr-amount_fee.html + /elements/select-part_pkg.html /edit/cust_main/first_pkg/select-part_pkg.html ); @@ -106,6 +107,26 @@ my %session_callbacks = ( }, + '/elements/select-part_pkg.html' => sub { + my( $custnum, $argsref ) = @_; + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return "unknown custnum $custnum"; + + my $pkgpart = $cust_main->agent->pkgpart_hashref; + + #false laziness w/ edit/cust_main/first_pkg.html + my @first_svc = ( 'svc_acct', 'svc_phone' ); + + my @part_pkg = + grep { $pkgpart->{ $_->pkgpart } + || ( $_->agentnum && $_->agentnum == $cust_main->agentnum ) + } + qsearch( 'part_pkg', { 'disabled' => '' }, '', 'ORDER BY pkg' ); # case? + + push @$argsref, 'part_pkg' => \@part_pkg; + ''; + }, + ); my $outbuf; diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 93f817de6..e2f859527 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -49,6 +49,8 @@ use FS::contact; use FS::cust_contact; use FS::cust_location; +use FS::ClientAPI::MyAccount::quotation; # just for code organization + $DEBUG = 0; $me = '[FS::ClientAPI::MyAccount]'; diff --git a/FS/FS/ClientAPI/MyAccount/quotation.pm b/FS/FS/ClientAPI/MyAccount/quotation.pm new file mode 100644 index 000000000..ce2debdde --- /dev/null +++ b/FS/FS/ClientAPI/MyAccount/quotation.pm @@ -0,0 +1,218 @@ +package FS::ClientAPI::MyAccount::quotation; + +use strict; +use FS::Record qw(qsearch qsearchs); +use FS::quotation; +use FS::quotation_pkg; + +our $DEBUG = 1; + +sub _custoragent_session_custnum { + FS::ClientAPI::MyAccount::_custoragent_session_custnum(@_); +} + +sub _quotation { + # the currently active quotation + my $session = shift; + my $quotation; + if ( my $quotationnum = $session->{'quotationnum'} ) { + $quotation = FS::quotation->by_key($quotationnum); + } + if ( !$quotation ) { + # find the last quotation created through selfservice + $quotation = qsearchs( 'quotation', { + 'custnum' => $session->{'custnum'}, + 'usernum' => $FS::CurrentUser::CurrentUser->usernum, + 'disabled' => '', + }); + warn "found selfservice quotation #". $quotation->quotationnum."\n" + if $quotation and $DEBUG; + } + if ( !$quotation ) { + $quotation = FS::quotation->new({ + 'custnum' => $session->{'custnum'}, + 'usernum' => $FS::CurrentUser::CurrentUser->usernum, + '_date' => time, + }); + $quotation->insert; # what to do on error? call the police? + warn "started new selfservice quotation #". $quotation->quotationnum."\n" + if $quotation and $DEBUG; + } + $session->{'quotationnum'} = $quotation->quotationnum; + return $quotation; +} + +=item quotation_info { session } + +Returns a hashref describing the current quotation, containing: + +- "sections", an arrayref containing one section for each billing frequency. + Each one will have: + - "description" + - "subtotal" + - "detail_items", an arrayref of detail items, each with: + - "pkgnum", the reference number (actually the quotationpkgnum field) + - "description", the package name (or tax name) + - "quantity" + - "amount" + +=cut + +sub quotation_info { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $quotation = _quotation($session); + return { 'error' => "No current quotation for this customer" } if !$quotation; + warn "quotation_info #".$quotation->quotationnum + if $DEBUG; + + # code reuse ftw + my $null_escape = sub { @_ }; + my ($sections) = $quotation->_items_sections(escape => $null_escape); + foreach my $section (@$sections) { + $section->{'detail_items'} = + [ $quotation->_items_pkg('section' => $section, escape_function => $null_escape) ]; + } + return { 'error' => '', 'sections' => $sections } +} + +=item quotation_print { session, 'format' } + +Renders the quotation. 'format' can be either 'html' or 'pdf'; the resulting +hashref will contain 'document' => the HTML or PDF contents. + +=cut + +sub quotation_print { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $quotation = _quotation($session); + return { 'error' => "No current quotation for this customer" } if !$quotation; + warn "quotation_print #".$quotation->quotationnum + if $DEBUG; + + my $format = $p->{'format'} + or return { 'error' => "No rendering format specified" }; + + my $document; + if ($format eq 'html') { + $document = $quotation->print_html; + } elsif ($format eq 'pdf') { + $document = $quotation->print_pdf; + } + warn "$format, ".length($document)." bytes\n" + if $DEBUG; + return { 'error' => '', 'document' => $document }; +} + +=item quotation_add_pkg { session, 'pkgpart', 'quantity', [ location opts ] } + +Adds a package to the user's current quotation. Session info and 'pkgpart' are +required. 'quantity' defaults to 1. + +Location can be specified as 'locationnum' to use an existing location, or +'address1', 'address2', 'city', 'state', 'zip', 'country' to create a new one, +or it will default to the customer's service location. + +=cut + +sub quotation_add_pkg { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $quotation = _quotation($session); + my $cust_main = $quotation->cust_main; + + my $pkgpart = $p->{'pkgpart'}; + my $allowed_pkgpart = $cust_main->agent->pkgpart_hashref; + + my $part_pkg = FS::part_pkg->by_key($pkgpart); + + if (!$part_pkg or !$allowed_pkgpart->{$pkgpart}) { + warn "disallowed quotation_pkg pkgpart $pkgpart\n" + if $DEBUG; + return { 'error' => "unknown package $pkgpart" }; + } + + warn "creating quotation_pkg with pkgpart $pkgpart\n" + if $DEBUG; + my $quotation_pkg = FS::quotation_pkg->new({ + 'quotationnum' => $quotation->quotationnum, + 'pkgpart' => $p->{'pkgpart'}, + 'quantity' => $p->{'quantity'} || 1, + }); + if ( $p->{locationnum} > 0 ) { + $quotation_pkg->set('locationnum', $p->{locationnum}); + } elsif ( $p->{address1} ) { + my $location = FS::cust_location->find_or_insert( + 'custnum' => $cust_main->custnum, + map { $_ => $p->{$_} } + qw( address1 address2 city county state zip country ) + ); + $quotation_pkg->set('locationnum', $location->locationnum); + } + + my $error = $quotation_pkg->insert + || $quotation->estimate; + + { 'error' => $error, + 'quotationnum' => $quotation->quotationnum }; +} + +=item quotation_remove_pkg { session, 'pkgnum' } + +Removes the package from the user's current quotation. 'pkgnum' is required. + +=cut + +sub quotation_remove_pkg { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $quotation = _quotation($session); + my $quotationpkgnum = $p->{pkgnum}; + my $quotation_pkg = FS::quotation_pkg->by_key($quotationpkgnum); + if (!$quotation_pkg + or $quotation_pkg->quotationnum != $quotation->quotationnum) { + return { 'error' => "unknown quotation item $quotationpkgnum" }; + } + warn "removing quotation_pkg with pkgpart ".$quotation_pkg->pkgpart."\n" + if $DEBUG; + + my $error = $quotation_pkg->delete + || $quotation->estimate; + + { 'error' => $error, + 'quotationnum' => $quotation->quotationnum }; +} + +=item quotation_order + +Convert the current quotation to a package order. + +=cut + +sub quotation_order { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $quotation = _quotation($session); + + my $error = $quotation->order; + + return { 'error' => $error }; +} + +1; diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 952b19940..5f1b38c0f 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -52,6 +52,7 @@ our %typefix = ( 'login_info' => \%typefix_skin_info, 'invoice_logo' => { 'logo' => 'base64', }, 'login_banner_image' => { 'image' => 'base64', }, + 'quotation_print' => { 'document' => 'base64' }, ); sub AUTOLOAD { @@ -186,6 +187,12 @@ sub ss2clientapi { 'call_time' => 'PrepaidPhone/call_time', 'call_time_nanpa' => 'PrepaidPhone/call_time_nanpa', 'phonenum_balance' => 'PrepaidPhone/phonenum_balance', + + 'quotation_info' => 'MyAccount/quotation/quotation_info', + 'quotation_print' => 'MyAccount/quotation/quotation_print', + 'quotation_add_pkg' => 'MyAccount/quotation/quotation_add_pkg', + 'quotation_remove_pkg' => 'MyAccount/quotation/quotation_remove_pkg', + 'quotation_order' => 'MyAccount/quotation/quotation_order', }; } diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index f2a96208f..45f35229f 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -695,22 +695,24 @@ sub estimate { # discounts if ( $cust_bill_pkg->get('discounts') ) { my $discount = $cust_bill_pkg->get('discounts')->[0]; - # discount records are generated as (setup, recur). - # well, not always, sometimes it's just (recur), but fixing this - # is horribly invasive. - my $qpd = $quotation_pkg_discount{$quotationpkgnum} - ||= qsearchs('quotation_pkg_discount', { - 'quotationpkgnum' => $quotationpkgnum - }); - - if (!$qpd) { #can't happen - warn "$me simulated bill returned a discount but no discount is in effect.\n"; - } - if ($discount and $qpd) { - if ( $i == 0 ) { - $qpd->set('setup_amount', $discount->amount); - } else { - $qpd->set('recur_amount', $discount->amount); + if ( $discount ) { + # discount records are generated as (setup, recur). + # well, not always, sometimes it's just (recur), but fixing this + # is horribly invasive. + my $qpd = $quotation_pkg_discount{$quotationpkgnum} + ||= qsearchs('quotation_pkg_discount', { + 'quotationpkgnum' => $quotationpkgnum + }); + + if (!$qpd) { #can't happen + warn "$me simulated bill returned a discount but no discount is in effect.\n"; + } + if ($discount and $qpd) { + if ( $i == 0 ) { + $qpd->set('setup_amount', $discount->amount); + } else { + $qpd->set('recur_amount', $discount->amount); + } } } } # end of discount stuff diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index 9d7e7ed17..a9da5643b 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -115,6 +115,13 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'start_thirdparty' => 'MyAccount/start_thirdparty', 'finish_thirdparty' => 'MyAccount/finish_thirdparty', + + 'quotation_info' => 'MyAccount/quotation/quotation_info', + 'quotation_print' => 'MyAccount/quotation/quotation_print', + 'quotation_add_pkg' => 'MyAccount/quotation/quotation_add_pkg', + 'quotation_remove_pkg' => 'MyAccount/quotation/quotation_remove_pkg', + 'quotation_order' => 'MyAccount/quotation/quotation_order', + ); @EXPORT_OK = ( keys(%autoload), diff --git a/ng_selfservice/images/cross.png b/ng_selfservice/images/cross.png new file mode 100644 index 000000000..1514d51a3 Binary files /dev/null and b/ng_selfservice/images/cross.png differ diff --git a/ng_selfservice/quotation.php b/ng_selfservice/quotation.php new file mode 100644 index 000000000..cf455431b --- /dev/null +++ b/ng_selfservice/quotation.php @@ -0,0 +1,130 @@ + + + + +quotation_info(array( + 'session_id' => $_COOKIE['session_id'], +)); + +$can_order = 0; + +if ( isset($quotation['sections']) and count($quotation['sections']) > 0 ) { + $can_order = 1; + # there are other ways this could be formatted, yes. + # if you want the HTML-formatted quotation, use quotation_print(). + print( + ''. + '

Order summary

'. + "\n" + ); + foreach ( $quotation['sections'] as $section ) { + print( + ''. + ''. + ''. + ''. + "\n" + ); + $row = 0; + foreach ( $section['detail_items'] as $detail ) { + print( + ''. + ''. + ''. + ''. + ''. "\n" + ); + $row = 1 - $row; + } + print( + ''. + ''. + ''. + ''. + ''. + '
'. htmlspecialchars($section['description']).'
' + ); + if ( $detail['pkgnum'] ) { + print( + ''. + '' + ); + } + print( + ''. htmlspecialchars($detail['description']). ''. $detail['amount']. '
Total'. $section['subtotal']. '
'. + "\n" + ); + } # foreach $section +} + +$pkgselect = $freeside->mason_comp( array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/select-part_pkg.html', + 'args' => array( 'onchange' , 'enable_order_pkg()', + 'empty_label' , 'Select package', + 'form_name' , 'AddPkgForm', + ), +)); +if ( isset($pkgselect['error']) && $pkgselect['error'] ) { + $error = $pkgselect['error']; + header('Location:index.php?error='. urlencode($pkgselect)); + die(); +} + +?> + + +
+ + +
+ +> +
+ + +
+> + + +
+ + + diff --git a/ng_selfservice/quotation_add_pkg.php b/ng_selfservice/quotation_add_pkg.php new file mode 100644 index 000000000..1e7e71fa9 --- /dev/null +++ b/ng_selfservice/quotation_add_pkg.php @@ -0,0 +1,31 @@ + $_COOKIE['session_id'], + 'pkgpart' => $_REQUEST['pkgpart'], + ); + + $results = $freeside->quotation_add_pkg($args); + + } + + if ( isset($results['error']) && $results['error'] ) { + $dest .= '?error=' . $results['error'] . ';pkgpart=' . $_REQUEST['pkgpart']; + } +} + +header("Location:$dest"); + +?> + diff --git a/ng_selfservice/quotation_order.php b/ng_selfservice/quotation_order.php new file mode 100644 index 000000000..d35eacbb2 --- /dev/null +++ b/ng_selfservice/quotation_order.php @@ -0,0 +1,15 @@ + $_COOKIE['session_id'] ); + +$results = $freeside->quotation_order($args); + +if ( isset($results['error']) && $results['error'] ) { + $dest = 'quotation.php?error=' . $results['error']; +} + +header("Location:$dest"); + +?> diff --git a/ng_selfservice/quotation_print.php b/ng_selfservice/quotation_print.php new file mode 100644 index 000000000..9676405d1 --- /dev/null +++ b/ng_selfservice/quotation_print.php @@ -0,0 +1,17 @@ + $_COOKIE['session_id'], + 'format' => 'pdf' +); + +$results = $freeside->quotation_print($args); +if ( isset($results['document']) ) { + header('Content-Type: application/pdf'); + header('Content-Disposition: filename=quotation.pdf'); + print($results['document']->scalar); +} else { + header("Location: quotation.php?error=" . $results['error']); +} + +?> diff --git a/ng_selfservice/quotation_remove_pkg.php b/ng_selfservice/quotation_remove_pkg.php new file mode 100644 index 000000000..07548c7f9 --- /dev/null +++ b/ng_selfservice/quotation_remove_pkg.php @@ -0,0 +1,31 @@ + $_COOKIE['session_id'], + 'pkgnum' => $_REQUEST['pkgnum'], + ); + + $results = $freeside->quotation_remove_pkg($args); + + } + + if ( isset($results['error']) && $results['error'] ) { + $dest .= '?error=' . $results['error']; + } + +} + +header("Location:$dest"); + +?>