From fb3f24328beb8e4d8703ea0d5376cdaaa86533a0 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 22 Apr 2015 19:46:28 -0500 Subject: RT#34134: Processing a Credit Card Payment on Accounts --- FS/FS/Conf.pm | 7 +++++++ httemplate/misc/payment.cgi | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index a37e5a6ef..c5c03ff08 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2853,6 +2853,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'manual_process-single_invoice_amount', + 'section' => 'billing', + 'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice', + 'type' => 'checkbox', + }, + { 'key' => 'manual_process-pkgpart', 'section' => 'billing', diff --git a/httemplate/misc/payment.cgi b/httemplate/misc/payment.cgi index 90b03c7e8..b83ad7166 100644 --- a/httemplate/misc/payment.cgi +++ b/httemplate/misc/payment.cgi @@ -273,7 +273,9 @@ my @states = sort { $a cmp $b } keys %states; my $amount = ''; if ( $balance > 0 ) { - $amount = $balance; + $amount = $balance + unless $conf->exists('manual_process-single_invoice_amount') + && ($cust_main->open_cust_bill != 1); } my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32; -- cgit v1.2.1 From 326075e45814387624303357207eae9069301f58 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 23 Apr 2015 11:10:39 -0700 Subject: calculate current day consistently for sync_bill_date + prorate_round_day, #34622 --- FS/FS/part_pkg/flat.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index d9d458809..930966a94 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -179,6 +179,12 @@ sub cutoff_day { if ( $self->option('sync_bill_date',1) ) { my $next_bill = $cust_pkg->cust_main->next_bill_date; if ( defined($next_bill) ) { + # careful here. if the prorate calculation is going to round to + # the nearest day, this needs to always return the same result + if ( $self->option('prorate_round_day', 1) ) { + my $hour = (localtime($next_bill))[2]; + $next_bill += 64800 if $hour >= 12; + } return (localtime($next_bill))[3]; } } -- cgit v1.2.1 From 4fda726fa9f8e709c68ec823edc5ae702723281c Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 24 Apr 2015 22:19:34 -0500 Subject: RT#34289: Flag service fields as mandatory --- FS/FS/Schema.pm | 1 + FS/FS/part_svc.pm | 16 +++++++++++- FS/FS/part_svc_column.pm | 3 +++ FS/FS/svc_Common.pm | 37 +++++++++++++++++++++++++-- FS/FS/svc_acct.pm | 6 ++++- FS/FS/svc_domain.pm | 5 +++- httemplate/browse/part_svc.cgi | 13 ++++++++-- httemplate/edit/elements/part_svc_column.html | 15 ++++++++--- httemplate/edit/part_svc.cgi | 20 +++++++++++++++ 9 files changed, 106 insertions(+), 10 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 7f28e11f7..42122f700 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3595,6 +3595,7 @@ sub tables_hashref { 'columnlabel', 'varchar', 'NULL', $char_d, '', '', 'columnvalue', 'varchar', 'NULL', 512, '', '', 'columnflag', 'char', 'NULL', 1, '', '', + 'required', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'columnnum', 'unique' => [ [ 'svcpart', 'columnname' ] ], diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index f56878acf..1da30cbb4 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -95,8 +95,12 @@ the part_svc_column table appropriately (see L). =item I__I - Default or fixed value for I in I. +=item I__I_label + =item I__I_flag - defines I__I action: null or empty (no default), `D' for default, `F' for fixed (unchangeable), , `S' for selectable choice, `M' for manual selection from inventory, or `A' for automatic selection from inventory. For virtual fields, can also be 'X' for excluded. +=item I__I_required - I should always have a true value + =back If you want to add part_svc_column records for fields that do not exist as @@ -145,6 +149,7 @@ sub insert { foreach my $field ( grep { $_ ne 'svcnum' && ( defined( $self->getfield($svcdb.'__'.$_.'_flag') ) + || defined($self->getfield($svcdb.'__'.$_.'_required')) || $self->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ ) } (fields($svcdb), @fields) ) { @@ -156,6 +161,7 @@ sub insert { my $flag = $self->getfield($svcdb.'__'.$field.'_flag'); my $label = $self->getfield($svcdb.'__'.$field.'_label'); + my $required = $self->getfield($svcdb.'__'.$field.'_required') ? 'Y' : ''; if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) { if ( uc($flag) =~ /^([A-Z])$/ ) { @@ -170,6 +176,8 @@ sub insert { $part_svc_column->setfield('columnlabel', $label) if $label !~ /^\s*$/; + $part_svc_column->setfield('required', $required); + if ( $previous ) { $error = $part_svc_column->replace($previous); } else { @@ -279,6 +287,7 @@ sub replace { foreach my $field ( grep { $_ ne 'svcnum' && ( defined( $new->getfield($svcdb.'__'.$_.'_flag') ) + || defined($new->getfield($svcdb.'__'.$_.'_required')) || $new->getfield($svcdb.'__'.$_.'_label') !~ /^\s*$/ ) } (fields($svcdb),@fields) ) { @@ -291,6 +300,7 @@ sub replace { my $flag = $new->getfield($svcdb.'__'.$field.'_flag'); my $label = $new->getfield($svcdb.'__'.$field.'_label'); + my $required = $new->getfield($svcdb.'__'.$field.'_required') ? 'Y' : ''; if ( uc($flag) =~ /^([A-Z])$/ || $label !~ /^\s*$/ ) { @@ -309,6 +319,8 @@ sub replace { $part_svc_column->setfield('columnlabel', $label) if $label !~ /^\s*$/; + $part_svc_column->setfield('required', $required); + if ( $previous ) { $error = $part_svc_column->replace($previous); } else { @@ -699,6 +711,8 @@ some components specified by "select-.*.html", and a bunch more... =item select_allow_empty - Used with select_table, adds an empty option +=item required - This field should always have a true value (do not use with type checkbox or disabled) + =back =cut @@ -773,7 +787,7 @@ sub process { and ref($param->{ $f }) ) { $param->{ $f } = join(',', @{ $param->{ $f } }); } - ( $f, $f.'_flag', $f.'_label' ); + ( $f, $f.'_flag', $f.'_label', $f.'_required' ); } @fields; diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm index 38ce1fa80..75a2dfb1a 100644 --- a/FS/FS/part_svc_column.pm +++ b/FS/FS/part_svc_column.pm @@ -45,6 +45,8 @@ fields are currently supported: =item columnflag - null or empty (no default), `D' for default, `F' for fixed (unchangeable), `S' for selectable choice, `M' for manual selection from inventory, `A' for automatic selection from inventory, or `H' for selection from a hardware class. For virtual fields, can also be 'X' for excluded. +=item required - column value expected to be true + =back =head1 METHODS @@ -91,6 +93,7 @@ sub check { || $self->ut_alpha('columnname') || $self->ut_textn('columnlabel') || $self->ut_anything('columnvalue') + || $self->ut_flag('required') ; return $error if $error; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 8199ba183..b1f9d146f 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -152,13 +152,46 @@ sub cust_linked { Checks the validity of fields in this record. -At present, this does nothing but call FS::Record::check (which, in turn, -does nothing but run virtual field checks). +Only checks fields marked as required in table_info or +part_svc_column definition. Should be invoked by service-specific +check using SUPER. Invokes FS::Record::check using SUPER. =cut sub check { my $self = shift; + + ## Checking required fields + + # get fields marked as required in table_info + my $required = {}; + my $labels = {}; + my $tinfo = $self->can('table_info') ? $self->table_info : {}; + my $fields = $tinfo->{'fields'} || {}; + foreach my $field (keys %$fields) { + if (ref($fields->{$field}) && $fields->{$field}->{'required'}) { + $required->{$field} = 1; + $labels->{$field} = $fields->{$field}->{'label'}; + } + } + # add fields marked as required in database + foreach my $column ( + qsearch('part_svc_column',{ + 'svcpart' => $self->svcpart, + 'required' => 'Y' + }) + ) { + $required->{$column->columnname} = 1; + $labels->{$column->columnname} = $column->columnlabel; + } + # do the actual checking + foreach my $field (keys %$required) { + unless ($self->$field) { + my $name = $labels->{$field} || $field; + return "Field $name is required\n" + } + } + $self->SUPER::check; } diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 452f250d8..790ac3468 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -283,6 +283,7 @@ sub table_info { disable_default => 1, disable_fixed => 1, disable_select => 1, + required => 1, }, 'password_selfchange' => { label => 'Password modification', type => 'checkbox', @@ -310,7 +311,9 @@ sub table_info { type => 'text', disable_inventory => 1, }, - '_password' => 'Password', + '_password' => { label => 'Password', + required => 1 + }, 'gid' => { label => 'GID', def_info => 'when blank, defaults to UID', @@ -333,6 +336,7 @@ sub table_info { select_key => 'svcnum', select_label => 'domain', disable_inventory => 1, + required => 1, }, 'pbxsvc' => { label => 'PBX', type => 'select-svc_pbx.html', diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm index b01d67310..78556cf8b 100644 --- a/FS/FS/svc_domain.pm +++ b/FS/FS/svc_domain.pm @@ -134,7 +134,10 @@ sub table_info { 'display_weight' => 20, 'cancel_weight' => 60, 'fields' => { - 'domain' => 'Domain', + 'domain' => { + label => 'Domain', + required => 1, + }, 'parent_svcnum' => { label => 'Parent domain / Communigate administrator domain', type => 'select', diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi index 0d3685355..ec5f321dd 100755 --- a/httemplate/browse/part_svc.cgi +++ b/httemplate/browse/part_svc.cgi @@ -61,6 +61,8 @@ function part_export_areyousure(href) { Modifier + Required + % my $conf = FS::Conf->new; % foreach my $part_svc ( @part_svc ) { @@ -78,6 +80,9 @@ function part_export_areyousure(href) { % $col->columnflag || ( $col->columnlabel !~ /^\S*$/ % && $col->columnlabel ne $def->{'label'} % ) +% || ( $col->required +% && !$def->{'required'} +% ) % ) % } % @dfields ; @@ -150,7 +155,7 @@ function part_export_areyousure(href) { % unless ( @fields ) { -% for ( 1..4 ) { +% for ( 1..5 ) { % } % } @@ -170,7 +175,6 @@ function part_export_areyousure(href) { <% $field %> <% $label %> <% $flag{$flag} %> - % my $value = &$formatter($part_svc->part_svc_column($field)->columnvalue); % if ( $flag =~ /^[MAH]$/ ) { @@ -189,6 +193,11 @@ function part_export_areyousure(href) { % } + +% if ($part_svc_column->required) { + Yes +% } + % $n1=""; % } #foreach $field % if ( $part_svc->restrict_edit_password ) { diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html index 2bb4f5e41..a6ccaf867 100644 --- a/httemplate/edit/elements/part_svc_column.html +++ b/httemplate/edit/elements/part_svc_column.html @@ -77,6 +77,7 @@ that field. Field Label Modifier + Required? % $part_svc->set('svcpart' => $opt{'clone'}) if $opt{'clone'}; # for now % my $i = 0; @@ -208,11 +209,19 @@ that field. 'empty_label' => "Select $mode class", 'multiple' => $multiple, &> +% } + + +% if (!$def->{'type'} || !(grep {$_ eq $def->{'type'}} ('checkbox','disabled'))) { + required || $def->{'required'}) ? 'CHECKED' : '' %> + <% $def->{'required'} ? 'DISABLED' : '' %> + > % } - + % if ( $def->{def_info} ) { (<% $def->{def_info} %>) @@ -228,7 +237,7 @@ that field. <% emt('Require "Provision" access right to edit password') %> - + restrict_edit_password ? 'CHECKED' : '' %>> @@ -244,7 +253,7 @@ that field. <% emt('This service has an attached router') %> - + has_router ? 'CHECKED' : '' %>> diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi index 47b020c5a..7a47f1550 100755 --- a/httemplate/edit/part_svc.cgi +++ b/httemplate/edit/part_svc.cgi @@ -101,6 +101,15 @@ function flag_changed(obj) { } } } + var required = document.getElementById(layer + '__' + field + '_required'); + if (required && !required.disabledinit) { + if (newflag == "F") { + required.checked = false; + required.disabled = true; + } else { + required.disabled = false; + } + } } window.onload = function() { @@ -111,6 +120,17 @@ window.onload = function() { obj.setAttribute('should_be_multiple', true); } } + var inputs = document.getElementsByTagName('INPUT'); + for(i = 0; i < inputs.length; i++) { + var obj = inputs[i]; + if (obj.type == 'checkbox') { + if ( obj.name.match(/_required$/) ) { + if ( obj.disabled ) { + obj.disabledinit = 1; + } + } + } + } for(i = 0; i < selects.length; i++) { var obj = selects[i]; if ( obj.name.match(/_flag$/) ) { -- cgit v1.2.1 From c110da0da864245e47cae019b8a347367cc6430c Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 25 Apr 2015 15:02:15 -0700 Subject: selfservice quotations, #33852 --- FS/FS/ClientAPI/MasonComponent.pm | 21 +++ FS/FS/ClientAPI/MyAccount.pm | 2 + FS/FS/ClientAPI/MyAccount/quotation.pm | 218 +++++++++++++++++++++++++++ FS/FS/ClientAPI_XMLRPC.pm | 7 + FS/FS/quotation.pm | 34 +++-- fs_selfservice/FS-SelfService/SelfService.pm | 7 + ng_selfservice/images/cross.png | Bin 0 -> 655 bytes ng_selfservice/quotation.php | 130 ++++++++++++++++ ng_selfservice/quotation_add_pkg.php | 31 ++++ ng_selfservice/quotation_order.php | 15 ++ ng_selfservice/quotation_print.php | 17 +++ ng_selfservice/quotation_remove_pkg.php | 31 ++++ 12 files changed, 497 insertions(+), 16 deletions(-) create mode 100644 FS/FS/ClientAPI/MyAccount/quotation.pm create mode 100644 ng_selfservice/images/cross.png create mode 100644 ng_selfservice/quotation.php create mode 100644 ng_selfservice/quotation_add_pkg.php create mode 100644 ng_selfservice/quotation_order.php create mode 100644 ng_selfservice/quotation_print.php create mode 100644 ng_selfservice/quotation_remove_pkg.php 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"); + +?> -- cgit v1.2.1 From 911b5f2429377b0b989e8a10e9971b2463e554a7 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 27 Apr 2015 00:05:46 -0700 Subject: disable quotation after ordering, #33852 --- FS/FS/ClientAPI/MyAccount/quotation.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FS/FS/ClientAPI/MyAccount/quotation.pm b/FS/FS/ClientAPI/MyAccount/quotation.pm index ce2debdde..787a0997c 100644 --- a/FS/FS/ClientAPI/MyAccount/quotation.pm +++ b/FS/FS/ClientAPI/MyAccount/quotation.pm @@ -211,6 +211,8 @@ sub quotation_order { my $quotation = _quotation($session); my $error = $quotation->order; + $quotation->set('disabled' => 'Y'); + $error ||= $quotation->replace; return { 'error' => $error }; } -- cgit v1.2.1