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
);
},
+ '/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;
use FS::cust_contact;
use FS::cust_location;
+use FS::ClientAPI::MyAccount::quotation; # just for code organization
+
$DEBUG = 0;
$me = '[FS::ClientAPI::MyAccount]';
--- /dev/null
+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;
'login_info' => \%typefix_skin_info,
'invoice_logo' => { 'logo' => 'base64', },
'login_banner_image' => { 'image' => 'base64', },
+ 'quotation_print' => { 'document' => 'base64' },
);
sub AUTOLOAD {
'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',
};
}
# 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
'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),
--- /dev/null
+<STYLE>
+td.amount {
+ text-align: right;
+}
+td.amount:before {
+ content: "$";
+}
+tr.total * {
+ background-color: #ddf;
+ font-weight: bold;
+}
+table.section {
+ width: 100%;
+ border-collapse: collapse;
+}
+table.section td {
+ font-size: small;
+ padding: 1ex 1ex;
+}
+table.section th {
+ text-align: left;
+ padding: 1ex;
+}
+.row0 td {
+ background-color: #eee;
+}
+.row1 td {
+ background-color: #fff;
+}
+</STYLE>
+
+<? $title ='Plan a new service order'; include('elements/header.php'); ?>
+<? $current_menu = 'services_new.php'; include('elements/menu.php'); ?>
+<?
+
+$quotation = $freeside->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(
+ '<INPUT STYLE="float: right" TYPE="button" onclick="window.location.href=\'quotation_print.php\'" value="Download a quotation" />'.
+ '<H3>Order summary</H3>'.
+ "\n"
+ );
+ foreach ( $quotation['sections'] as $section ) {
+ print(
+ '<TABLE CLASS="section">'.
+ '<TR>'.
+ '<TH COLSPAN=4>'. htmlspecialchars($section['description']).'</TH>'.
+ '</TR>'.
+ "\n"
+ );
+ $row = 0;
+ foreach ( $section['detail_items'] as $detail ) {
+ print(
+ '<TR CLASS="row' . $row . '">'.
+ '<TD>'
+ );
+ if ( $detail['pkgnum'] ) {
+ print(
+ '<A HREF="quotation_remove_pkg.php?pkgnum=' .
+ $detail['pkgnum'] . '">'.
+ '<IMG SRC="images/cross.png" /></A>'
+ );
+ }
+ print(
+ '</TD>'.
+ '<TD>'. htmlspecialchars($detail['description']). '</TD>'.
+ '<TD CLASS="amount">'. $detail['amount']. '</TD>'.
+ '</TR>'. "\n"
+ );
+ $row = 1 - $row;
+ }
+ print(
+ '<TR CLASS="total">'.
+ '<TD></TD>'.
+ '<TD>Total</TD>'.
+ '<TD CLASS="amount">'. $section['subtotal']. '</TD>'.
+ '</TR>'.
+ '</TABLE>'.
+ "\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();
+}
+
+?>
+<SCRIPT TYPE="text/javascript">
+function enable_order_pkg () {
+ document.AddPkgForm.submit.disabled =
+ (document.AddPkgForm.pkgpart.value == '');
+}
+</SCRIPT>
+
+<DIV STYLE="border-top: 1px solid; padding: 1ex">
+<? $error = $_REQUEST['error']; include('elements/error.php'); ?>
+
+<FORM NAME="AddPkgForm" ACTION="quotation_add_pkg.php" METHOD=POST>
+<? echo $pkgselect['output']; ?>
+<INPUT NAME="submit" TYPE="submit" VALUE="Add package" <? if ( ! isset($_REQUEST['pkgpart']) ) { echo 'DISABLED'; } ?>>
+</FORM>
+
+<? if ( $can_order ) { ?>
+<FORM NAME="OrderQuoteForm" ACTION="quotation_order.php" METHOD=POST>
+<INPUT TYPE="submit" VALUE="Confirm this order" <? if ( !$can_order ) { echo 'DISABLED'; } ?>>
+<? } ?>
+
+</DIV>
+
+<? include('elements/menu_footer.php'); ?>
+<? include('elements/footer.php'); ?>
--- /dev/null
+<? require('elements/session.php');
+
+$dest = 'quotation.php';
+
+if ( isset($_REQUEST['pkgpart']) ) {
+
+ $results = array();
+
+ $params = array( 'custnum', 'pkgpart' );
+
+ $matches = array();
+ if ( preg_match( '/^(\d+)$/', $_REQUEST['pkgpart'] ) ) {
+
+ $args = array(
+ 'session_id' => $_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");
+
+?>
+
--- /dev/null
+<? require('elements/session.php');
+
+$dest = 'services.php';
+
+$args = array( 'session_id' => $_COOKIE['session_id'] );
+
+$results = $freeside->quotation_order($args);
+
+if ( isset($results['error']) && $results['error'] ) {
+ $dest = 'quotation.php?error=' . $results['error'];
+}
+
+header("Location:$dest");
+
+?>
--- /dev/null
+<? require('elements/session.php');
+
+$args = array(
+ 'session_id' => $_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']);
+}
+
+?>
--- /dev/null
+<? require('elements/session.php');
+
+$dest = 'quotation.php';
+
+if ( isset($_REQUEST['pkgnum']) ) {
+
+ $results = array();
+
+ $params = array( 'custnum', 'pkgnum' );
+
+ $matches = array();
+ if ( preg_match( '/^(\d+)$/', $_REQUEST['pkgnum'] ) ) {
+
+ $args = array(
+ 'session_id' => $_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");
+
+?>