From 69e481a4a9191b9912d6bb8202627a5dc75f74ce Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Thu, 11 Jan 2018 20:05:34 -0600 Subject: rt# 74031 implement svc_realestate --- FS/FS/Mason.pm | 3 + FS/FS/Schema.pm | 90 ++++++++++- FS/FS/h_svc_realestate.pm | 31 ++++ FS/FS/realestate_location.pm | 177 +++++++++++++++++++++ FS/FS/realestate_unit.pm | 163 +++++++++++++++++++ FS/FS/svc_Common.pm | 10 +- FS/FS/svc_realestate.pm | 172 ++++++++++++++++++++ FS/t/realestate_location.t | 5 + FS/t/realestate_unit.t | 5 + FS/t/svc_realestate.t | 5 + httemplate/browse/realestate_location.html | 43 +++++ httemplate/browse/realestate_unit.html | 70 ++++++++ httemplate/docs/part_svc-table.html | 7 +- httemplate/edit/process/realestate_location.html | 14 ++ httemplate/edit/process/realestate_unit.html | 13 ++ httemplate/edit/realestate_location.html | 72 +++++++++ httemplate/edit/realestate_unit.html | 48 ++++++ httemplate/elements/menu.html | 6 +- .../elements/select-realestate_location.html | 32 ++++ httemplate/elements/select-realestate_unit.html | 59 +++++++ .../elements/tr-select-realestate_location.html | 17 ++ httemplate/elements/tr-select-realestate_unit.html | 5 + 22 files changed, 1042 insertions(+), 5 deletions(-) create mode 100644 FS/FS/h_svc_realestate.pm create mode 100644 FS/FS/realestate_location.pm create mode 100644 FS/FS/realestate_unit.pm create mode 100644 FS/FS/svc_realestate.pm create mode 100644 FS/t/realestate_location.t create mode 100644 FS/t/realestate_unit.t create mode 100644 FS/t/svc_realestate.t create mode 100644 httemplate/browse/realestate_location.html create mode 100644 httemplate/browse/realestate_unit.html create mode 100644 httemplate/edit/process/realestate_location.html create mode 100644 httemplate/edit/process/realestate_unit.html create mode 100644 httemplate/edit/realestate_location.html create mode 100644 httemplate/edit/realestate_unit.html create mode 100644 httemplate/elements/select-realestate_location.html create mode 100644 httemplate/elements/select-realestate_unit.html create mode 100644 httemplate/elements/tr-select-realestate_location.html create mode 100644 httemplate/elements/tr-select-realestate_unit.html diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 7bdb6059e..7f883dec1 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -418,6 +418,9 @@ if ( -e $addl_handler_use_file ) { use FS::part_svc_msgcat; use FS::commission_schedule; use FS::commission_rate; + use FS::realestate_location; + use FS::realestate_unit; + use FS::svc_realestate; use FS::saved_search; use FS::sector_coverage; # Sammath Naur diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 6d7520bd9..f7ac973be 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -495,8 +495,44 @@ sub tables_hashref { my $username_len = 64; #usernamemax config file - # name type nullability length default local + # Return a hashref defining the entire application database schema + # Each key of the hashref contains a structure describing a database table + # + # table_name => { + # columns => [...], + # primary_key => 'column', + # unique => [column,column,...], + # index => [[column],[column,column],...], + # foreign_keys => [{...},{...},...], + # } + # + # + # columns => [ + # + # 'column_name', + # + # 'column_type', + # + # 'NULL' or '', # 'NULL' : Allow null values + # # '' : Disallow null values + # + # 'length', # Column size value. eg: + # # 40 : VARCHAR(40) + # # '10,2' : FLOAT(10,2) + # + # 'default', # Default column value for a new record + # # (Unclear if setting this to '' results in a default + # # value of NULL or empty string?) + # + # '', # local ? + # + # name, type, nullability, length, default, local, + # name, type, nullability, length, default, local, + # ... + # + # ], + # name type nullability length default local return { 'agent' => { @@ -7601,6 +7637,57 @@ sub tables_hashref { 'foreign_keys' => [], }, + 'realestate_unit' => { + 'columns' => [ + 'realestatenum', 'serial', '', '', '', '', + 'realestatelocnum', 'int', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'unit_title', 'varchar', '', $char_d, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + 'primary_key' => 'realestatenum', + 'unique' => [ ['unit_title'] ], + 'index' => [ + ['agentnum'], + ['realestatelocnum'], + ['disabled'], + ['unit_title'], + ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + {columns => ['realestatelocnum'] => table => 'realestate_location'}, + ], + }, + + realestate_location => { + 'columns' => [ + 'realestatelocnum', 'serial', '', '', '', '', + 'agentnum', 'int', 'NULL', '', '', '', + 'location_title', 'varchar', '', $char_d, '', '', + 'address1', 'varchar', 'NULL', $char_d, '', '', + 'address2', 'varchar', 'NULL', $char_d, '', '', + 'city', 'varchar', 'NULL', $char_d, '', '', + 'state', 'varchar', 'NULL', $char_d, '', '', + 'zip', 'char', 'NULL', 5, '', '', + 'disabled', 'char', 'NULL', 1, '', '', + ], + primary_key => 'realestatelocnum', + 'unique' => [ ['location_title'] ], + 'index' => [ ['agentnum'], ['disabled'] ], + 'foreign_keys' => [ + {columns => ['agentnum'], table => 'agent'}, + ], + }, + + svc_realestate => { + columns => [ + 'svcnum', 'serial', '', '', '', '', + 'realestatenum', 'int', 'NULL', '', '', '', + ], + primary_key => 'svcnum', + index => [], + }, + # name type nullability length default local #'new_table' => { @@ -7627,4 +7714,3 @@ L =cut 1; - diff --git a/FS/FS/h_svc_realestate.pm b/FS/FS/h_svc_realestate.pm new file mode 100644 index 000000000..2fdd291d1 --- /dev/null +++ b/FS/FS/h_svc_realestate.pm @@ -0,0 +1,31 @@ +package FS::h_svc_realestate; + +use strict; +use vars qw( @ISA ); +use FS::h_Common; + + +@ISA = qw( FS::h_Common ); + +sub table { 'h_svc_realestate' }; + +=head1 NAME + +FS::h_svc_circuit - Historical realestate service objects + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +An FS::h_svc_realestate object + +=head1 BUGS + +=head1 SEE ALSO + +L, L, L, schema.html from the base +documentation. + +=cut + +1; diff --git a/FS/FS/realestate_location.pm b/FS/FS/realestate_location.pm new file mode 100644 index 000000000..d9cd76a58 --- /dev/null +++ b/FS/FS/realestate_location.pm @@ -0,0 +1,177 @@ +package FS::realestate_location; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; + +use FS::Record qw(qsearchs qsearch); + +=head1 NAME + +FS::realestate_location - Object representing a realestate_location record + +=head1 SYNOPSIS + + use FS::realestate_location; + + $location = new FS::realestate_location \%values; + $location = new FS::realestate_location { + agentnum => 1, + location_title => 'Superdome', + address1 => '1500 Sugar Bowl Dr', + city => 'New Orleans', + state => 'LA', + zip => '70112', + }; + + $error = $location->insert; + $error = $new_loc->replace($location); + $error = $record->check; + + $error = $location->add_unit('Box Seat No. 42'); + @units = $location->units; + +=head1 DESCRIPTION + +An FS::realestate_location object represents a location for one or more +FS::realestate_unit objects. Expected to contain at least one unit, as only +realestate_unit objects are assignable to packages via +L. + +FS::realestate_location inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatelocnum + +=item agentnum + +=item location_title + +=item address1 (optional) + +=item address2 (optional) + +=item city (optional) + +=item state (optional) + +=item zip (optional) + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L) + +=cut + +sub table {'realestate_location';} + +=item insert (see L) + +=item delete + + FS::realestate_location records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_location records should never be deleted"; +} + +=item replace OLD_RECORD (see L) + +=item check (see L) + +=item agent + +Returns the associated agent + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return exists $self->{agent} + ? $self->{agent} + : $self->{agent} = qsearchs('agent', {agentnum => $self->agentnum} ); +} + + +=item add_unit UNIT_TITLE + +Create an associated L record + +=cut + +sub add_unit { + my ($self, $unit_title) = @_; + croak "add_unit() requires a \$unit_title parameter" unless $unit_title; + + if ( + qsearchs('realestate_unit',{ + realestatelocnum => $self->realestatelocnum, + unit_title => $unit_title, + }) + ) { + return "Unit Title ($unit_title) has already been used for location (". + $self->location_title.")"; + } + + my $unit = FS::realestate_unit->new({ + realestatelocnum => $self->realestatelocnum, + agentnum => $self->agentnum, + unit_title => $unit_title, + }); + my $err = $unit->insert; + die "Error creating FS::realestate_new record: $err" if $err; + + return; +} + + +=item units + +Returns all units associated with this location + +=cut + +sub units { + my $self = shift; + return qsearch( + 'realestate_unit', + {realestatelocnum => $self->realestatelocnum} + ); +} + + +=head1 SUBROUTINES + +=over 4 + +=cut + + + + +=back + +=head1 SEE ALSO + +L, L, L + +=cut + +1; diff --git a/FS/FS/realestate_unit.pm b/FS/FS/realestate_unit.pm new file mode 100644 index 000000000..d1d1f7fda --- /dev/null +++ b/FS/FS/realestate_unit.pm @@ -0,0 +1,163 @@ +package FS::realestate_unit; +use strict; +use warnings; +use Carp qw(croak); + +use base 'FS::Record'; +use FS::Record qw(qsearch qsearchs); + +=head1 NAME + +FS::realestate_unit - Object representing a realestate_unit record + +=head1 SYNOPSIS + + use FS::realestate_unit; + + $record = new FS:realestate_unit \%values; + $record = new FS::realestate_unit { + realestatelocnum => 42, + agentnum => 1, + unit_title => 'Ste 404', + }; + + $error = $record->insert; + $error = $new_rec->replace($record) + $error = $record->check; + + $location = $record->location; + +=head1 DESCRIPTION + +An FS::realestate_unit object represents an invoicable unit of real estate. +Object may represent a single property, such as a rental house. It may also +represent a group of properties sharing a common address or identifier, such +as a shopping mall, apartment complex, or office building, concert hall. + +A FS::realestate_unit object must be associated with a FS::realestate_location + +FS::realestate_unit inherits from FS::Record. + +The following fields are currently supported: + +=over 4 + +=item realestatenum + +=item realestatelocnum + +=item agentnum + +=item unit_title + +=item disabled + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF (see L) + +=cut + +sub table {'realestate_unit';} + +=item insert (see L) + +=item delete + + FS::realestate_unit records should never be deleted, only disabled + +=cut + +sub delete { + # Once this record has been associated with a customer in any way, it + # should not be deleted. todo perhaps, add a is_deletable function that + # checks if the record has ever actually been used, and allows deletion + # if it hasn't. (entered in error, etc). + croak "FS::realestate_unit records should never be deleted"; +} + + +=item replace OLD_RECORD (see L) + +=item check (see L) + +=item agent + +Returns the associated agent, if any, for this object + +=cut + +sub agent { + my $self = shift; + return undef unless $self->agentnum; + return qsearchs('agent', {agentnum => $self->agentnum} ); +} + +=item location + + Return the associated FS::realestate_location object + +=cut + +sub location { + my $self = shift; + return $self->{location} if exists $self->{location}; + return $self->{location} = qsearchs( + 'realestate_location', + {realestatelocnum => $self->realestatelocnum} + ); +} + +=back + +=item custnum + +Pull the assigned custnum for this unit, if provisioned + +=cut + +sub custnum { + my $self = shift; + return $self->{custnum} + if $self->{custnum}; + + # select cust_pkg.custnum + # from svc_realestate + # LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum + # LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum + # WHERE svc_realestate.realestatenum = $realestatenum + + my $row = qsearchs({ + select => 'cust_pkg.custnum', + table => 'svc_realestate', + addl_from => 'LEFT JOIN cust_svc ON svc_realestate.svcnum = cust_svc.svcnum ' + . 'LEFT JOIN cust_pkg ON cust_svc.pkgnum = cust_pkg.pkgnum ', + extra_sql => 'WHERE svc_realestate.realestatenum = '.$self->realestatenum, + }); + + return + unless $row && $row->custnum; + + return $self->{custnum} = $row->custnum; +} + +=head1 SUBROUTINES + +=over 4 + +=cut + + +=back + +=head1 SEE ALSO + +L, L, L + +=cut + +1; diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index f2456a56f..afd5db64f 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -122,6 +122,15 @@ sub virtual_fields { =item label +Returns a label to identify a record of this service. +Label may be displayed on freeside screens, and within customer bills. + +For example, $obj->label may return: + + - A provisioned phone number for svc_phone + - The mailing list name and e-mail address for svc_mailinglist + - The address of a rental property svc_realestate + svc_Common provides a fallback label subroutine that just returns the svcnum. =cut @@ -1586,4 +1595,3 @@ from the base documentation. =cut 1; - diff --git a/FS/FS/svc_realestate.pm b/FS/FS/svc_realestate.pm new file mode 100644 index 000000000..a7512eef8 --- /dev/null +++ b/FS/FS/svc_realestate.pm @@ -0,0 +1,172 @@ +package FS::svc_realestate; +use base qw(FS::svc_Common); + +use strict; +use warnings; +use vars qw($conf); + +use FS::Record qw(qsearchs qsearch dbh); +use Tie::IxHash; + +$FS::UID::callback{'FS::svc_realestate'} = sub { + $conf = new FS::Conf; +}; + +=head1 NAME + +FS::svc_realestate - Object methods for svc_realestate records + +=head1 SYNOPSIS + + {...} TODO + +=head1 DESCRIPTION + +A FS::svc_realestate object represents a billable real estate trasnaction, +such as renting a home or office. + +FS::svc_realestate inherits from FS::svc_Common. The following fields are +currently supported: + +=over 4 + +=item svcnum - primary key + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Instantiates a new svc_realestate object. + +=cut + +sub table_info { + tie my %fields, 'Tie::IxHash', + svcnum => 'Service', + realestatenum => { + type => 'select-realestate_unit', + label => 'Real estate unit', + }; + + { + name => 'Real estate', + name_plural => 'Real estate services', + longname_plural => 'Real estate services', + display_weight => 100, + cancel_weight => 100, + fields => \%fields, + }; +} + +sub table {'svc_realestate'} + +=item label + +Returns a label formatted as: + + +=cut + +sub label { + my $self = shift; + my $unit = $self->realestate_unit; + my $location = $self->realestate_location; + + return $location->location_title.' '.$unit->unit_title + if $unit && $location; + + return $self->svcnum; # shouldn't happen +} + + +=item realestate_unit + +Returns associated L + +=cut + +sub realestate_unit { + my $self = shift; + + return $self->get('_realestate_unit') + if $self->get('_realestate_unit'); + + return unless $self->realestatenum; + + my $realestate_unit = qsearchs( + 'realestate_unit', + {realestatenum => $self->realestatenum} + ); + + $self->set('_realestate_unit', $realestate_unit); + $realestate_unit; +} + +=item realestate_location + +Returns associated L + +=cut + +sub realestate_location { + my $self = shift; + + my $realestate_unit = $self->realestate_unit; + return unless $realestate_unit; + + $realestate_unit->location; +} + +=item cust_svc + +Returns associated L + +=cut + +sub cust_svc { + qsearchs('cust_svc', { 'svcnum' => $_[0]->svcnum } ); +} + +=item search_sql + +I have an unfounded suspicion this method serves no purpose in this context + +=cut + +# sub search_sql {die "search_sql called on FS::svc_realestate"} + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=item delete + +Delete this record from the database. + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=back 4 + +=head1 BUGS + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; diff --git a/FS/t/realestate_location.t b/FS/t/realestate_location.t new file mode 100644 index 000000000..ecb1d8be9 --- /dev/null +++ b/FS/t/realestate_location.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_location; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/realestate_unit.t b/FS/t/realestate_unit.t new file mode 100644 index 000000000..bbecc1a4c --- /dev/null +++ b/FS/t/realestate_unit.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::realestate_unit; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/svc_realestate.t b/FS/t/svc_realestate.t new file mode 100644 index 000000000..4145d8b52 --- /dev/null +++ b/FS/t/svc_realestate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::svc_realestate; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/browse/realestate_location.html b/httemplate/browse/realestate_location.html new file mode 100644 index 000000000..be2cd11f8 --- /dev/null +++ b/httemplate/browse/realestate_location.html @@ -0,0 +1,43 @@ +<% include( 'elements/browse.html', + title => emt('Real Estate Locations'), + name => 'real estate locations', + + menubar => [ + 'Edit units' => "${p}browse/realestate_unit.html", + 'Add a new location' => "${p}edit/realestate_location.html", + 'Add a new unit' => "${p}edit/realestate_unit.html", + ], + + query => { table => 'realestate_location' }, + count_query => 'SELECT COUNT(*) FROM realestate_location', + + header => [ 'Location', 'Address', 'Address 2', 'City', 'State', 'Zip' ], + fields => [ + 'location_title', + 'address1', + 'address2', + 'city', + 'state', + 'zip' + ], + links => [ + ["${p}edit/realestate_location.html?", 'realestatelocnum' ], + ], + + agent_virt => 1, + agent_pos => 0, + disableable => 1, +) +%> +<%init> + + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + + + + diff --git a/httemplate/browse/realestate_unit.html b/httemplate/browse/realestate_unit.html new file mode 100644 index 000000000..399cd2583 --- /dev/null +++ b/httemplate/browse/realestate_unit.html @@ -0,0 +1,70 @@ +<% include( 'elements/browse.html', + title => emt('Real Estate Inventory'), + name => 'real estate inventory', + + menubar => [ + 'Edit locations' => "${p}browse/realestate_location.html", + 'Add a new location' => "${p}edit/realestate_location.html", + 'Add a new unit' => "${p}edit/realestate_unit.html", + ], + + query => { + table => 'realestate_unit', + select => join(', ',qw( + realestate_unit.* + realestate_location.location_title + cust_main.first + cust_main.last + cust_main.company + )), + addl_from => " + LEFT JOIN realestate_location + ON realestate_location.realestatelocnum + = realestate_unit.realestatelocnum + LEFT JOIN svc_realestate + ON realestate_unit.realestatenum = svc_realestate.realestatenum + LEFT JOIN cust_svc + ON svc_realestate.svcnum = cust_svc.svcnum + LEFT JOIN cust_pkg + ON cust_svc.pkgnum = cust_pkg.pkgnum + LEFT JOIN cust_main + ON cust_pkg.custnum = cust_main.custnum + ", + order_by => "ORDER BY location_title, unit_title" + }, + + count_query => 'SELECT COUNT(*) FROM realestate_unit', + + header => [ 'Location', 'Unit', 'Customer' ], + fields => [ + 'location_title', + 'unit_title', + sub { + return '' unless $_[0]->custnum; + return $_[0]->company if $_[0]->company; + return $_[0]->first.' '.$_[0]->last; + }, + ], + links => [ + ["${p}edit/realestate_location.html?", 'realestatelocnum' ], + ["${p}edit/realestate_unit.html?", 'realestatenum' ], + ["${p}view/cust_main.cgi?", 'custnum' ] + ], + + agent_virt => 1, + agent_pos => 0, + disableable => 1, +) +%> +<%init> + + +my $curuser = $FS::CurrentUser::CurrentUser; +die("access denied") + unless $curuser->access_right('Edit inventory') + || $curuser->access_right('Edit global inventory') + || $curuser->access_right('Configuration'); + + + + diff --git a/httemplate/docs/part_svc-table.html b/httemplate/docs/part_svc-table.html index 820d0b9cc..56a4d0e8c 100644 --- a/httemplate/docs/part_svc-table.html +++ b/httemplate/docs/part_svc-table.html @@ -39,6 +39,7 @@ Hosting Colocation + Real Estate
    @@ -54,6 +55,11 @@
  • svc_port: Customer router/switch port
+ +
    +
  • svc_realestate: Real estate properties +
+ + + + + + + \ No newline at end of file diff --git a/min_selfservice/payment_cc.php b/min_selfservice/payment_cc.php new file mode 100644 index 000000000..f47e83c2c --- /dev/null +++ b/min_selfservice/payment_cc.php @@ -0,0 +1,120 @@ + + +process_payment(array( + 'session_id' => $_COOKIE['session_id'], + 'payby' => 'CARD', + 'amount' => $_POST['amount'], + 'payinfo' => $_POST['payinfo'], + 'paycvv' => $_POST['paycvv'], + 'month' => $_POST['month'], + 'year' => $_POST['year'], + 'payname' => $_POST['payname'], + 'address1' => $_POST['address1'], + 'address2' => $_POST['address2'], + 'city' => $_POST['city'], + 'state' => $_POST['state'], + 'zip' => $_POST['zip'], + 'country' => $_POST['country'], + 'save' => $_POST['save'], + 'auto' => $_POST['auto'], + 'paybatch' => $_POST['paybatch'], + //'discount_term' => $discount_term, + )); + + if ( $payment_results['error'] ) { + $payment_error = $payment_results['error']; + } else { + $receipt_html = $payment_results['receipt_html']; + } + +} + +if ( $receipt_html ) { ?> + + Your payment was processed successfully. Thank you.

+ + +payment_info( array( + 'session_id' => $_COOKIE['session_id'], + 'payment_payby' => 'CARD', + ) ); + + if ( isset($payment_info['error']) && $payment_info['error'] ) { + $error = $payment_info['error']; + header('Location:index.php?error='. urlencode($error)); + die(); + } + + extract($payment_info); + + $error = $payment_error; + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $balance ], + )); + //$tr_amount_fee = $tr_amount_fee->{'error'} || $tr_amount_fee->{'output'}; + $tr_amount_fee = $tr_amount_fee['output']; + + ?> + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
Amount Due +
+ $ +
+
Card type + +
+ NAME="save" VALUE="1"> + Remember this card and billing address +
+ NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }"> + Charge future payments to this card automatically +
+
+ + + + + + + + \ No newline at end of file diff --git a/min_selfservice/payment_finish.php b/min_selfservice/payment_finish.php new file mode 100644 index 000000000..04fdfa6f3 --- /dev/null +++ b/min_selfservice/payment_finish.php @@ -0,0 +1,34 @@ + + +finish_thirdparty($params); + + if ( isset($payment_results['error']) ) { + $error = $payment_results['error']; + include('elements/error.php'); + } else { +?> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/min_selfservice/payment_paypal.php b/min_selfservice/payment_paypal.php new file mode 100644 index 000000000..7a70f9852 --- /dev/null +++ b/min_selfservice/payment_paypal.php @@ -0,0 +1,41 @@ + + +start_thirdparty(array( + 'session_id' => $_COOKIE['session_id'], + 'method' => 'PAYPAL', + 'amount' => $_POST['amount'], + )); + + include('elements/post_thirdparty.php'); + +} else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + ) ); + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $payment_info['balance'] ], + )); + $tr_amount_fee = $tr_amount_fee['output']; + + include('elements/error.php'); ?> + +
Your payment details
Payment #
Payment amount + $ +
Processing # +
+ + + + + +
Amount Due$
+
+ + + + + \ No newline at end of file diff --git a/min_selfservice/payment_webpay.php b/min_selfservice/payment_webpay.php new file mode 100644 index 000000000..e4343fcb4 --- /dev/null +++ b/min_selfservice/payment_webpay.php @@ -0,0 +1,41 @@ + + +start_thirdparty(array( + 'session_id' => $_COOKIE['session_id'], + 'method' => 'CC', + 'amount' => $_POST['amount'], + )); + + include('elements/post_thirdparty.php'); + +} else { + + $payment_info = $freeside->payment_info( array( + 'session_id' => $_COOKIE['session_id'], + ) ); + + $tr_amount_fee = $freeside->mason_comp(array( + 'session_id' => $_COOKIE['session_id'], + 'comp' => '/elements/tr-amount_fee.html', + 'args' => [ 'amount', $payment_info['balance'] ], + )); + $tr_amount_fee = $tr_amount_fee['output']; + + include('elements/error.php'); ?> +
+ + + + + + +
Amount Due$
+
+ +
+ + + \ No newline at end of file -- cgit v1.2.1 From 697515200e6e405272bd2d1cdf9784a990057334 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 19 Jan 2018 01:34:48 -0600 Subject: rt# 78547 Implement report listing future auto-bill charges --- httemplate/elements/menu.html | 2 + httemplate/search/elements/grid-report.html | 8 +- httemplate/search/future_autobill.html | 189 ++++++++++++++++++++++++++ httemplate/search/report_future_autobill.html | 42 ++++++ 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 httemplate/search/future_autobill.html create mode 100644 httemplate/search/report_future_autobill.html diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 0a73d71a6..9b8b2cd1e 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -418,6 +418,8 @@ if( $curuser->access_right('Financial reports') ) { $report_financial{'Customer Accounting Summary'} = [ $fsurl.'search/report_customer_accounting_summary.html', 'Customer accounting summary report' ]; + $report_financial{'Upcoming Auto-Bill Transactions'} = [ $fsurl.'search/report_future_autobill.html', 'Upcoming auto-bill transactions' ]; + } elsif($curuser->access_right('Receivables report')) { $report_financial{'A/R Aging'} = [ $fsurl.'search/report_receivables.html', 'Accounts Receivable Aging report' ]; diff --git a/httemplate/search/elements/grid-report.html b/httemplate/search/elements/grid-report.html index 98e81785f..b1e543012 100644 --- a/httemplate/search/elements/grid-report.html +++ b/httemplate/search/elements/grid-report.html @@ -161,7 +161,7 @@ as ">Excel spreadsheet
.shaded { background-color: #c8c8c8; } .totalshaded { background-color: #bfc094; } - +
% foreach my $rowinfo (@rows) { {class} ? ' class="'.$rowinfo->{class}.'"' : ''%>> % my $thisrow = shift @cells; @@ -172,7 +172,11 @@ as ">Excel spreadsheet
% $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1; % $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1; % $style .= ' class="' . $cell->{class} . '"' if $cell->{class}; +% if ($cell->{bypass_filter}) { + <<%$td%><%$style%>><% $cell->{value} %>> +% } else { <<%$td%><%$style%>><% $cell->{value} |h %>> +% } % } % } @@ -186,4 +190,6 @@ $title @cells $head => '' $foot => '' +$table_width => "100%" +$table_class => "report" diff --git a/httemplate/search/future_autobill.html b/httemplate/search/future_autobill.html new file mode 100644 index 000000000..711a25f82 --- /dev/null +++ b/httemplate/search/future_autobill.html @@ -0,0 +1,189 @@ +<%doc> + +Report listing upcoming auto-bill transactions + +Spec requested the ability to run this report with a longer date range, +and see which charges will process on which day. Checkbox multiple_billing_dates +enables this functionality. + +Performance: +This is a dynamically generated report. The time this report takes to run +will depends on the number of customers. Installations with a high number +of auto-bill customers may find themselves unable to run this report +because of browser timeout. Report could be implemented as a queued job if +necessary, to solve the performance problem. + + +<& elements/grid-report.html, + title => 'Upcoming auto-bill transactions', + rows => \@rows, + cells => \@cells, + table_width => "", + table_class => 'gridreport', + head => ' + + ', +&> + +<%init> + +use FS::UID qw( dbh myconnect ); + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + + my $target_dt; + my @target_dates; + + # Work with all date/time operations @ 12 noon + my %noon = ( + hour => 12, + minute => 0, + second => 0 + ); + + my $now_dt = DateTime->now; + $now_dt = DateTime->new( + month => $now_dt->month, + day => $now_dt->day, + year => $now_dt->year, + %noon, + ); + + # Get target date from form + if ($cgi->param('target_date')) { + my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date'); + $target_dt = DateTime->new( + month => $mm, + day => $dd, + year => $yy, + %noon, + ) if $mm && $dd & $yy; + + # Catch a date from the past: time only travels in one direction + $target_dt = undef if $target_dt->epoch < $now_dt->epoch; + } + + # without a target date, default to tomorrow + unless ($target_dt) { + $target_dt = DateTime->from_epoch( epoch => time() + 86400) ; + $target_dt = DateTime->new( + month => $target_dt->month, + day => $target_dt->day, + year => $target_dt->year, + %noon + ); + } + + # If multiple_billing_dates checkbox selected, create a range of dates + # from today until the given report date. Otherwise, use target date only. + if ($cgi->param('multiple_billing_dates')) { + my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch); + until ($walking_dt->epoch > $target_dt->epoch) { + push @target_dates, $walking_dt->epoch; + $walking_dt->add(days => 1); + } + } else { + push @target_dates, $target_dt->epoch; + } + + # List all customers with an auto-bill method + # + # my %cust_payby = map {$_->custnum => $_} qsearch({ + # table => 'cust_payby', + # hashref => { + # weight => { op => '>', value => '0' }, + # paydate => { op => '>', value => $target_dt->ymd }, + # }, + # order_by => " ORDER BY weight DESC ", + # }); + + # List all customers with an auto-bill method that's not expired + my %cust_payby = map {$_->custnum => $_} qsearch({ + table => 'cust_payby', + hashref => { + weight => { op => '>', value => '0' }, + }, + order_by => " ORDER BY weight DESC ", + extra_sql => " AND ( payby = 'CHEK' OR ( paydate > '".$target_dt->ymd."')) ", + }); + + my %abreport; + my @rows; + + local $@; + local $SIG{__DIE__}; + my $temp_dbh = myconnect(); + eval { # Creating sandbox dbh where all connections are to be rolled back + local $FS::UID::dbh = $temp_dbh; + local $FS::UID::AutoCommit = 0; + + # Generate report data into @rows + for my $custnum (keys %cust_payby) { + my $cust_main = qsearchs('cust_main', {custnum => $custnum}); + + # walk forward through billing dates + for my $query_epoch (@target_dates) { + my $return_bill = []; + + eval { # Don't let an error on one customer crash the report + my $error = $cust_main->bill( + time => $query_epoch, + return_bill => $return_bill, + no_usage_reset => 1, + ); + die "$error (simulating future billing)" if $error; + }; + warn ("$@: (future_autobill custnum:$custnum)"); + + if (@{$return_bill}) { + my $inv = $return_bill->[0]; + push @rows,{ + name => $cust_main->name, + _date => $inv->_date, + cells => [ + { class => 'gridreport', value => $custnum }, + { class => 'gridreport', + value => ''.$cust_main->name.'', + bypass_filter => 1, + }, + { class => 'gridreport', value => $inv->charged, format => 'money' }, + { class => 'gridreport', value => DateTime->from_epoch(epoch=>$inv->_date)->ymd }, + { class => 'gridreport', value => ($cust_payby{$custnum}->payby || $cust_payby{$custnum}->paytype) }, + { class => 'gridreport', value => $cust_payby{$custnum}->paymask }, + ] + }; + } + + } + $temp_dbh->rollback; + } # /foreach $custnum + + }; # /eval + warn("$@") if $@; + + # Sort output by date, and format for output to grid-report.html + my @cells = [ + # header row + { class => 'gridreport', value => '#', header => 1 }, + { class => 'gridreport', value => 'Name', header => 1 }, + { class => 'gridreport', value => 'Amount', header => 1 }, + { class => 'gridreport', value => 'Date', header => 1 }, + { class => 'gridreport', value => 'Type', header => 1 }, + { class => 'gridreport', value => 'Account', header => 1 }, + ]; + push @cells, + map { $_->{cells} } + sort { $a->{_date} <=> $b->{_date} || $a->{name} cmp $b->{name} } + @rows; + + # grid-report.html requires a parallel @rows parameter to accompany @cells + @rows = map { {class => 'gridreport'} } 1..scalar(@cells); + + diff --git a/httemplate/search/report_future_autobill.html b/httemplate/search/report_future_autobill.html new file mode 100644 index 000000000..1a0c9f48a --- /dev/null +++ b/httemplate/search/report_future_autobill.html @@ -0,0 +1,42 @@ +<%doc> + +Display date selector for the future_autobill.html report + + +<% include('/elements/header.html', 'Future Auto-Bill Transactions' ) %> + + +
+
+<& /elements/tr-input-date-field.html, + { + name => 'target_date', + value => $target_date, + label => emt('Target billing date').': ', + required => 1 + } +&> + +<& /elements/tr-checkbox.html, + 'label' => emt('Multiple billing dates (slow)').': ', + 'field' => 'multiple_billing_dates', + 'value' => '1', +&> + +
+ +
+ + + + +<% include('/elements/footer.html') %> + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); + +my $target_date = DateTime->from_epoch(epoch=>(time()+86400))->mdy('/'); + + -- cgit v1.2.1 From ea963c68bda696def555b86a3b5a7410386ab92d Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 19 Jan 2018 15:52:53 -0500 Subject: RT# 76398 - Added auto status field or status table creation --- FS/FS/cdr/Import.pm | 142 ++++++++++++++++++++++++++++------------- FS/bin/freeside-cdr-freeswitch | 11 ++-- 2 files changed, 103 insertions(+), 50 deletions(-) diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index f32a7bd85..aa5f5744f 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -19,7 +19,7 @@ FS::cdr::Import - CDR importing use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'mysql', #Pg, Sybase, etc. + 'dbd' => 'Pg', #mysql, Sybase, etc. 'table' => 'TABLE_NAME', 'primary_key' => 'BILLING_ID', 'status_table' = > 'STATUS_TABLE_NAME', # if using a table rather than field in main table @@ -46,61 +46,64 @@ sub dbi_import { my %opt; #opt is specified for each install / run of the script getopts('H:U:P:D:T:c:L:S:', \%opt); - my $user = shift(@ARGV) or die $class->cli_usage; - - $opt{D} ||= $args{database}; - - #do we want to add more types? or add as we go? - my %dbi_connect_types = { - 'Sybase' => ':server', - 'Pg' => ':host', - }; - - my $dsn = 'dbi:'. $args{dbd}; - my $dbi_connect_type = $dbi_connect_types{$args{'dbd'}} ? $dbi_connect_types{$args{'dbd'}} : ':host'; - $dsn .= $dbi_connect_type . "=$opt{H}"; - $dsn .= ";database=$opt{D}" if $opt{D}; + my $user = shift(@ARGV) or die $class->cli_usage; + my $database = $opt{D} || $args{database}; + my $table = $opt{T} || $args{table}; + my $pkey = $args{primary_key}; + my $pkey_info = $args{primary_key_info} ? $args{primary_key_info} : 'BIGINT'; + my $status_table = $opt{S} || $args{status_table}; + my $dbd_type = $args{'dbd'} ? $args{'dbd'} : 'Pg'; + my $status_column = $args{status_column} ? $args{status_column} : 'freesidestatus'; + my $status_column_info = $args{status_column_info} ? $args{status_column} : 'VARCHAR(32)'; + + my $queries = get_queries({ + 'dbd' => $dbd_type, + 'table' => $table, + 'status_column' => $status_column, + 'status_column_info' => $status_column_info, + 'status_table' => $status_table, + 'primary_key' => $pkey, + 'primary_key_info' => $pkey_info, + }); + + my $dsn = 'dbi:'. $dbd_type; + $dsn .= $queries->{connect_type} . "=$opt{H}"; + $dsn .= ";database=$database" if $database; my $dbi = DBI->connect($dsn, $opt{U}, $opt{P}) or die $DBI::errstr; adminsuidsetup $user; - #my $fsdbh = FS::UID::dbh; - - my $table = $opt{T} || $args{table}; - my $pkey = $args{primary_key}; - my $status_table = $opt{S} || $args{status_table}; - - #just doing this manually with IVR MSSQL databases for now - # # check for existence of freesidestatus - # my $status = $dbi->selectall_arrayref("SHOW COLUMNS FROM $table WHERE Field = 'freesidestatus'"); - # if( ! @$status ) { - # print "Adding freesidestatus column...\n"; - # $dbi->do("ALTER TABLE $table ADD COLUMN freesidestatus varchar(32)") - # or die $dbi->errstr; - # } - # else { - # print "freesidestatus column present\n"; - # } - # or if using a status_table: - # CREATE TABLE FREESIDE_BILLING ( - # BILLING_ID BIGINT, - # FREESIDESTATUS VARCHAR(32) - # ) + ## check for status table if using. if not there create it. + if ($status_table) { + my $status = $dbi->selectall_arrayref( $queries->{check_statustable} ); + if( ! @$status ) { + print "Adding status table $status_table ...\n"; + $dbi->do( $queries->{create_statustable} ) + or die $dbi->errstr; + } + } + ## check for column freeside status if not using status table and create it if not there. + else { + my $status = $dbi->selectall_arrayref( $queries->{check_statuscolumn} ); + if( ! @$status ) { + print "Adding $status_column column...\n"; + $dbi->do( $queries->{create_statuscolumn} ) + or die $dbi->errstr; + } + } #my @cols = values %{ $args{column_map} }; my $sql = "SELECT $table.* FROM $table "; # join(',', @cols). " FROM $table ". - $sql .= 'LEFT JOIN '. $status_table. - " ON ( $table.$pkey = ". $status_table. ".$pkey )" + $sql .= "LEFT JOIN $status_table ON ( $table.$pkey = $status_table.$pkey ) " if $status_table; - $sql .= ' WHERE freesidestatus IS NULL '; + $sql .= "WHERE $status_column IS NULL "; #$sql .= ' LIMIT '. $opt{L} if $opt{L}; my $sth = $dbi->prepare($sql); $sth->execute or die $sth->errstr. " executing $sql"; - #MySQL-specific print "Importing ".$sth->rows." records...\n"; my $cdr_batch = new FS::cdr_batch({ 'cdrbatch' => $args{batch_name} . '-import-'. time2str('%Y/%m/%d-%T',time), @@ -123,6 +126,7 @@ sub dbi_import { } $hash{$field} = '' if $hash{$field} =~ /^\s+$/; #IVR (MSSQL?) bs } + my $cdr = FS::cdr->new(\%hash); $cdr->cdrtypenum($opt{c}) if $opt{c}; @@ -145,12 +149,12 @@ sub dbi_import { if ( $status_table ) { $st_sql = - 'INSERT INTO '. $status_table. " ( $pkey, freesidestatus ) ". + 'INSERT INTO '. $status_table. " ( $pkey, $status_column ) ". " VALUES ( ?, 'done' )"; } else { - $st_sql = "UPDATE $table SET freesidestatus = 'done' WHERE $pkey = ?"; + $st_sql = "UPDATE $table SET $status_column = 'done' WHERE $pkey = ?"; } @@ -179,10 +183,58 @@ sub cli_usage { "Usage: \n $0\n\t-H hostname\n\t[ -D database ]\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\t[ -L num_cdrs_limit ]\n\t[ -T table ]\n\t[ -S status table ]\n\tfreesideuser\n"; } +sub get_queries { + #my ($dbd, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; + my $info = shift; + + #do we want to add more types? or add as we go? + my %dbi_connect_types = ( + 'Sybase' => ':server', + 'Pg' => ':host', + ); + + #Check for freeside status table Sybase has not been tested + my %dbi_check_statustable = ( + 'Sybase' => "SELECT systables.name FROM sysobjects + JOIN systables ON sysobjects.id = systables.id + WHERE sysobjects.name LIKE '$info->{table}' AND systables.name = $info->{status_table}", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", + ); + + #Check for freeside status table Sybase has not been tested + my %dbi_create_statustable = ( + 'Sybase' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + 'Pg' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", + ); + + #Check for freeside status column Sybase has not been tested + my %dbi_check_statuscolumn = ( + 'Sybase' => "SELECT syscolumns.name FROM sysobjects + JOIN syscolumns ON sysobjects.id = syscolumns.id + WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = $info->{status_column}", + 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{table}' AND column_name = '$info->{status_column}' ", + ); + + #Check for freeside status column Sybase has not been tested + my %dbi_create_statuscolumn = ( + 'Sybase' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + ); + + my $queries = { + 'connect_type' => $dbi_connect_types{$info->{dbd}}, + 'check_statustable' => $dbi_check_statustable{$info->{dbd}}, + 'create_statustable' => $dbi_create_statustable{$info->{dbd}}, + 'check_statuscolumn' => $dbi_check_statuscolumn{$info->{dbd}}, + 'create_statuscolumn' => $dbi_create_statuscolumn{$info->{dbd}}, + }; + + return $queries; +} + =head1 BUGS -Not everything has been refactored out of the various bin/cdr-*.import scripts, -let alone other places. +This has only been test with Pg -> postgresql databases Sparse documentation. diff --git a/FS/bin/freeside-cdr-freeswitch b/FS/bin/freeside-cdr-freeswitch index 7f09578d4..51f360ca7 100644 --- a/FS/bin/freeside-cdr-freeswitch +++ b/FS/bin/freeside-cdr-freeswitch @@ -5,11 +5,12 @@ use Date::Parse 'str2time'; use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'Pg', - 'database' => 'fusionpbx', - 'table' => 'v_xml_cdr', - 'primary_key' => 'uuid', - 'column_map' => { #freeside => fusionpbx + 'dbd' => 'Pg', + 'database' => 'fusionpbx', + 'table' => 'v_xml_cdr', + 'status_column' => 'freesidestatus', + 'primary_key' => 'uuid',:q + 'column_map' => { #freeside => fusionpbx #'cdrid' => 'uuid', #Primary key #'' => 'CALL_SESSION_ID', # Call Session Id (unique per call session – GUID) 'uniqueid' => 'uuid', # -- cgit v1.2.1 From c7caf18eb75426b50715e3bf02c2a2f92e170b83 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 23 Jan 2018 14:05:02 -0500 Subject: RT# 76398 - fixed sybase query for detecting tables and adding columns --- FS/FS/cdr/Import.pm | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index aa5f5744f..c0c192f40 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -195,9 +195,7 @@ sub get_queries { #Check for freeside status table Sybase has not been tested my %dbi_check_statustable = ( - 'Sybase' => "SELECT systables.name FROM sysobjects - JOIN systables ON sysobjects.id = systables.id - WHERE sysobjects.name LIKE '$info->{table}' AND systables.name = $info->{status_table}", + 'Sybase' => "SELECT * FROM sysobjects WHERE name = $info->{status_table}", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", ); @@ -217,7 +215,7 @@ sub get_queries { #Check for freeside status column Sybase has not been tested my %dbi_create_statuscolumn = ( - 'Sybase' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", + 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info}", 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", ); -- cgit v1.2.1 From ca5b77df888ac14882c290f10f625654e39d9072 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Wed, 24 Jan 2018 14:45:43 -0500 Subject: RT# 76398 - Fixed sybase connection and cleaned up code --- FS/FS/cdr/Import.pm | 54 +++++++++++++++++++++++------------------- FS/bin/freeside-cdr-freeswitch | 2 +- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/FS/FS/cdr/Import.pm b/FS/FS/cdr/Import.pm index c0c192f40..f2263c552 100644 --- a/FS/FS/cdr/Import.pm +++ b/FS/FS/cdr/Import.pm @@ -19,15 +19,19 @@ FS::cdr::Import - CDR importing use FS::cdr::Import; FS::cdr::Import->dbi_import( - 'dbd' => 'Pg', #mysql, Sybase, etc. - 'table' => 'TABLE_NAME', - 'primary_key' => 'BILLING_ID', - 'status_table' = > 'STATUS_TABLE_NAME', # if using a table rather than field in main table - 'column_map' => { #freeside => remote_db - 'freeside_column' => 'remote_db_column', - 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, + 'dbd' => 'Pg', #mysql, Sybase, etc. + 'database' => 'DATABASE_NAME', + 'table' => 'TABLE_NAME',, + 'status_table' => 'STATUS_TABLE_NAME', # if using a table rather than field in main table + 'primary_key' => 'BILLING_ID', + 'primary_key_info' => 'BIGINT', # defaults to bigint + 'status_column' => 'STATUS_COLUMN_NAME', # defaults to freesidestatus + 'status_column_info' => 'varchar(32)', # defaults to varchar(32) + 'column_map' => { #freeside => remote_db + 'freeside_column' => 'remote_db_column', + 'freeside_column' => sub { my $row = shift; $row->{remote_db_column}; }, }, - 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. + 'batch_name' => 'batch_name', # cdr_batch name -import-date gets appended. ); =head1 DESCRIPTION @@ -59,6 +63,7 @@ sub dbi_import { my $queries = get_queries({ 'dbd' => $dbd_type, + 'host' => $opt{H}, 'table' => $table, 'status_column' => $status_column, 'status_column_info' => $status_column_info, @@ -67,8 +72,7 @@ sub dbi_import { 'primary_key_info' => $pkey_info, }); - my $dsn = 'dbi:'. $dbd_type; - $dsn .= $queries->{connect_type} . "=$opt{H}"; + my $dsn = 'dbi:'. $dbd_type . $queries->{connect_type}; $dsn .= ";database=$database" if $database; my $dbi = DBI->connect($dsn, $opt{U}, $opt{P}) @@ -178,44 +182,46 @@ sub dbi_import { } sub cli_usage { - #"Usage: \n $0\n\t[ -H hostname ]\n\t-D database\n\t-U user\n\t-P password\n\tfreesideuser\n"; - #"Usage: \n $0\n\t-H hostname\n\t-D database\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\tfreesideuser\n"; "Usage: \n $0\n\t-H hostname\n\t[ -D database ]\n\t-U user\n\t-P password\n\t[ -c cdrtypenum ]\n\t[ -L num_cdrs_limit ]\n\t[ -T table ]\n\t[ -S status table ]\n\tfreesideuser\n"; } sub get_queries { - #my ($dbd, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; + #my ($dbd, $host, $table, $column, $column_create_info, $status_table, $primary_key, $primary_key_info) = @_; my $info = shift; - #do we want to add more types? or add as we go? + #get host and port information. + my ($host, $port) = split /:/, $info->{host}; + $host ||= 'localhost'; + $port ||= '5000'; # check for pg default 5000 is sybase. + my %dbi_connect_types = ( - 'Sybase' => ':server', - 'Pg' => ':host', + 'Sybase' => ':host='.$host.';port='.$port, + 'Pg' => ':host='.$info->{host}, ); - #Check for freeside status table Sybase has not been tested + #Check for freeside status table my %dbi_check_statustable = ( - 'Sybase' => "SELECT * FROM sysobjects WHERE name = $info->{status_table}", + 'Sybase' => "SELECT * FROM sysobjects WHERE name = '$info->{status_table}'", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{status_table}' AND column_name = '$info->{status_column}'", ); - #Check for freeside status table Sybase has not been tested + #Create freeside status table my %dbi_create_statustable = ( 'Sybase' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", 'Pg' => "CREATE TABLE $info->{status_table} ( $info->{primary_key} $info->{primary_key_info}, $info->{status_column} $info->{status_column_info} )", ); - #Check for freeside status column Sybase has not been tested + #Check for freeside status column my %dbi_check_statuscolumn = ( 'Sybase' => "SELECT syscolumns.name FROM sysobjects JOIN syscolumns ON sysobjects.id = syscolumns.id - WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = $info->{status_column}", + WHERE sysobjects.name LIKE '$info->{table}' AND syscolumns.name = '$info->{status_column}'", 'Pg' => "SELECT * FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '$info->{table}' AND column_name = '$info->{status_column}' ", ); - #Check for freeside status column Sybase has not been tested + #Create freeside status column my %dbi_create_statuscolumn = ( - 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info}", + 'Sybase' => "ALTER TABLE $info->{table} ADD $info->{status_column} $info->{status_column_info} NULL", 'Pg' => "ALTER TABLE $info->{table} ADD COLUMN $info->{status_column} $info->{status_column_info}", ); @@ -232,7 +238,7 @@ sub get_queries { =head1 BUGS -This has only been test with Pg -> postgresql databases +currently works with Pg(Postgresql) and Sybase(Sybase AES) Sparse documentation. diff --git a/FS/bin/freeside-cdr-freeswitch b/FS/bin/freeside-cdr-freeswitch index 51f360ca7..3c18ef2a6 100644 --- a/FS/bin/freeside-cdr-freeswitch +++ b/FS/bin/freeside-cdr-freeswitch @@ -9,7 +9,7 @@ FS::cdr::Import->dbi_import( 'database' => 'fusionpbx', 'table' => 'v_xml_cdr', 'status_column' => 'freesidestatus', - 'primary_key' => 'uuid',:q + 'primary_key' => 'uuid', 'column_map' => { #freeside => fusionpbx #'cdrid' => 'uuid', #Primary key #'' => 'CALL_SESSION_ID', # Call Session Id (unique per call session – GUID) -- cgit v1.2.1 From 7b5f402ed6fa6e9b82d724b7a14b341be819e5be Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 25 Jan 2018 12:06:08 -0500 Subject: RT#79121 - added POD documentation --- FS/bin/freeside-cdr-aninetworks-import | 66 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import index 1e6217648..a8fc0cbfd 100755 --- a/FS/bin/freeside-cdr-aninetworks-import +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -55,30 +55,34 @@ my $ls = $sftp->ls('.', wanted => qr/^UYM.*.zip$/i, names_only =>1 ); my @files = @$ls; warn scalar(@files)." CDR files found.\n" if $opt_v; -# apply date range +# apply date range from last downloaded batch. if ( $opt_a ) { my $most_recent = $previous[0]; if ($most_recent) { - if ($most_recent->cdrbatch =~ /^ani_networks-(\d+)/) { + if ($most_recent->cdrbatch =~ /^*Daily_(\d+)_/) { my $date = $1; warn "limiting to dates > $date (from most recent batch)\n" if $opt_v; @files = grep { /^*Daily_(\d+)_/ && $1 > $date } @files; } - } # else download them all + } } + +# apply a start date if given if ( $opt_s ) { - # start date # normalize date format $opt_s = time2str('%Y%m%d', str2time($opt_s)) if $opt_s =~ /\D/; warn "limiting to dates > $opt_s\n" if $opt_v; @files= grep { /^*Daily_(\d+)_/ && $1 >= $opt_s } @files; } + +# apply a end date if given if ( $opt_e ) { - # end date + # normalize date format $opt_e = time2str('%Y%m%d', str2time($opt_e)) if $opt_e =~ /\D/; warn "limiting to dates < $opt_e\n" if $opt_v; @files= grep { /^*Daily_(\d+)_/ && $1 < $opt_e } @files; } + warn scalar(@files) ." files to be downloaded\n" if $opt_v; foreach my $file (@files) { @@ -164,3 +168,55 @@ sub sftp { $sftp; } +=head1 NAME + +freeside-cdr-aninetworks-import - Download CDR files from a remote server via SFTP + +=head1 SYNOPSIS + + freeside-cdr-aninetworks-import [ -v ] [ -d ] [ -a ] [ -c cdrtypenum ] + [ -s startdate ] [ -e enddate ] user sftpuser sftppassword + +=head1 DESCRIPTION + +Command line tool to download CDR files from a remote server via SFTP +and then import them into the database. + +-v: be verbose + +-d: enable sftp debugging (very noisy) + +-a: automatically choose start date from most recently downloaded batch + +-c: cdrtypenum to set, defaults to none + +-s: if specified, sets a startdate. startdate starts at 00:00:00 + +-e: if specified, sets a enddate. enddate starts at 00:00:00 so if you wish to include enddate must add one more day. + +user: freeside username + +sftpuser: sftp user for arkftp.aninetworks.com + +sftppassword: password for sftp user + +=head1 EXAMPLES + +freeside-cdr-aninetworks-import -a +will get all cdr files starting from the day of the last day processed. + +freeside-cdr-aninetworks-import -s 20180120 -e 20180121 +will get all cdr files from 01/20/2018 + +freeside-cdr-aninetworks-import -v -s $(date --date="-1 day" +\%Y\%m\%d) -e $(date +\%Y\%m\%d) +will get all cdr files from yesterday + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; \ No newline at end of file -- cgit v1.2.1 From 72c6f125423327ed4e89170d484bc5e4a5760bf2 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 25 Jan 2018 12:27:36 -0500 Subject: RT# 79121 - fixed error with loading files with option a --- FS/bin/freeside-cdr-aninetworks-import | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FS/bin/freeside-cdr-aninetworks-import b/FS/bin/freeside-cdr-aninetworks-import index a8fc0cbfd..b5fc226a4 100755 --- a/FS/bin/freeside-cdr-aninetworks-import +++ b/FS/bin/freeside-cdr-aninetworks-import @@ -61,8 +61,8 @@ if ( $opt_a ) { if ($most_recent) { if ($most_recent->cdrbatch =~ /^*Daily_(\d+)_/) { my $date = $1; - warn "limiting to dates > $date (from most recent batch)\n" if $opt_v; - @files = grep { /^*Daily_(\d+)_/ && $1 > $date } @files; + warn "limiting to dates >= $date (from most recent batch)\n" if $opt_v; + @files = grep { /^*Daily_(\d+)_/ && $1 >= $date } @files; } } } -- cgit v1.2.1 From f975ff916152bf85b76e5c7bea48f7fdc8af1cc2 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Fri, 26 Jan 2018 16:24:28 -0500 Subject: RT# 75095 - updated mapping of ooma import file format --- FS/FS/cust_main/Import_Charges.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index 0a12c8752..f90e0496f 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -68,7 +68,9 @@ sub batch_charge { if ( $format eq 'simple' ) { @fields = qw( custnum agent_custid amount pkg ); } elsif ( $format eq 'ooma' ) { - @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); + ##should charges to charge be a config option? %charges = ( 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', -- cgit v1.2.1 From e517b3733dd3d19a7af586c9f4b8cf9982a8b3d2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 27 Jan 2018 21:21:59 -0600 Subject: RT# 73421 Add allow messages flag message_dest to contact_email --- FS/FS/Schema.pm | 3 ++- FS/FS/contact.pm | 7 +++---- FS/FS/cust_contact.pm | 7 ++++++- httemplate/edit/process/cust_main-contacts.html | 8 ++++++++ httemplate/elements/contact.html | 6 ++++-- httemplate/view/cust_main/contacts_new.html | 2 ++ 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index f7ac973be..65eadad21 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1793,7 +1793,8 @@ sub tables_hashref { 'classnum', 'int', 'NULL', '', '', '', 'comment', 'varchar', 'NULL', 255, '', '', 'selfservice_access', 'char', 'NULL', 1, '', '', - 'invoice_dest', 'char', 'NULL', 1, '', '', + 'invoice_dest', 'char', 'NULL', 1, '', '', # Y or NULL + 'message_dest', 'char', 'NULL', 1, '', '', # Y or NULL ], 'primary_key' => 'custcontactnum', 'unique' => [ [ 'custnum', 'contactnum' ], ], diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 44c538806..fa047f59d 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -155,7 +155,7 @@ sub insert { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest)) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -430,7 +430,7 @@ sub replace { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access invoice_dest )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest )) { $link_hash{$_} = $self->get($_); $self->$_(''); } @@ -955,7 +955,7 @@ sub cgi_contact_fields { my @contact_fields = qw( classnum first last title comment emailaddress selfservice_access - invoice_dest password + invoice_dest message_dest password ); push @contact_fields, 'phonetypenum'. $_->phonetypenum @@ -1028,4 +1028,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_contact.pm b/FS/FS/cust_contact.pm index 118a9e000..adad46e9e 100644 --- a/FS/FS/cust_contact.pm +++ b/FS/FS/cust_contact.pm @@ -59,6 +59,11 @@ empty or Y 'Y' if the customer should get invoices sent to this address, null if not +=item message_dest + +'Y' if contact should get non-invoice email messages sent to this address, +NULL if not + =back =head1 METHODS @@ -119,6 +124,7 @@ sub check { || $self->ut_textn('comment') || $self->ut_enum('selfservice_access', [ '', 'Y' ]) || $self->ut_flag('invoice_dest') + || $self->ut_flag('message_dest') ; return $error if $error; @@ -148,4 +154,3 @@ L, L, L =cut 1; - diff --git a/httemplate/edit/process/cust_main-contacts.html b/httemplate/edit/process/cust_main-contacts.html index 2a7185b5c..5b8319f5a 100644 --- a/httemplate/edit/process/cust_main-contacts.html +++ b/httemplate/edit/process/cust_main-contacts.html @@ -1,3 +1,11 @@ +<%doc> + + This form works indirectly with the tables contact_email and + contact_phone. The columns are updated against an FS::contact + object. The insert/update methods of FS::contact will make the + necessary inserts/updates to contact_email and contact_phone. + + <% include('elements/process.html', 'table' => 'cust_main', 'error_redirect' => popurl(3). 'edit/cust_main-contacts.html?', diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html index faee7ead4..43e520155 100644 --- a/httemplate/elements/contact.html +++ b/httemplate/elements/contact.html @@ -42,7 +42,8 @@ % $value = join(', ', map $_->emailaddress, $contact->contact_email); % } elsif ( $field eq 'selfservice_access' % or $field eq 'comment' -% or $field eq 'invoice_dest' ) { +% or $field eq 'invoice_dest' +% or $field eq 'message_dest' ) { % $value = $X_contact->get($field); % } else { % $value = $contact->get($field); @@ -78,7 +79,7 @@ return false } -% } elsif ( $field eq 'invoice_dest' ) { +% } elsif ( $field eq 'invoice_dest' || $field eq 'message_dest' ) { % my $curr_value = $cgi->param($name . '_' . $field); % $curr_value = $value if !defined($curr_value); <& select.html, @@ -168,6 +169,7 @@ tie my %label, 'Tie::IxHash', unless ($opt{'for_prospect'}) { $label{'invoice_dest'} = 'Send invoices'; + $label{'message_dest'} = 'Send messages'; $label{'selfservice_access'} = 'Self-service'; } diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index 94488670d..a28b44934 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -11,6 +11,7 @@ <%$th%>Email <%$th%>Send invoices <%$th%>Self-service + <%$th%>Send messages % foreach my $phone_type (@phone_type) { <%$th%><% $phone_type->typename |h %> % } @@ -32,6 +33,7 @@ % my @contact_email = $contact->contact_email; <%$td%><% join(', ', map $_->emailaddress, @contact_email) %> <%$td%><% $cust_contact->invoice_dest eq 'Y' ? 'Yes' : 'No' %> + <%$td%><% $cust_contact->message_dest eq 'Y' ? 'Yes' : 'No' %> <%$td%> % if ( $cust_contact->selfservice_access ) { Enabled -- cgit v1.2.1 From 1a4faacda4f3affc6060e9f539bd3ae6c71408f7 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 28 Jan 2018 02:26:01 -0600 Subject: RT# 73421 Simulated SMTP server for testing E-Mail --- bin/fakesmtpserver.pl | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 bin/fakesmtpserver.pl diff --git a/bin/fakesmtpserver.pl b/bin/fakesmtpserver.pl new file mode 100755 index 000000000..1f2ca3f31 --- /dev/null +++ b/bin/fakesmtpserver.pl @@ -0,0 +1,53 @@ +#!/usr/bin/env perl + +=head1 Fake SMTP Server + +While this script is running, creates an SMTP server at localhost port 25. + +Can only accept one client connection at a time. If necessary, +it could be updated to fork on client connections. + +When an e-mail is delivered, the TO and FROM are printed to STDOUT. +The TO, FROM and MSG are saved to a file in $message_save_dir + +=cut + +use strict; +use warnings; + +use Carp; +use Net::SMTP::Server; +use Net::SMTP::Server::Client; +use Net::SMTP::Server::Relay; + +my $message_save_dir = '/home/freeside/fakesmtpserver'; + +mkdir $message_save_dir, 0777; + +my $server = new Net::SMTP::Server('localhost', 25) || + croak("Unable to handle client connection: $!\n"); + +while(my $conn = $server->accept()) { + my $client = new Net::SMTP::Server::Client($conn) || + croak("Unable to handle client connection: $!\n"); + + $client->process || next; + + open my $fh, '>', $message_save_dir.'/'.time().'.txt' + or die "error: $!"; + + for my $f (qw/TO FROM/) { + + if (ref $client->{$f} eq 'ARRAY') { + print "$f: $_\n" for @{$client->{$f}}; + print $fh "$f: $_\n" for @{$client->{$f}}; + } else { + print "$f: $client->{$f}\n"; + print $fh "$f: $client->{$f}\n"; + } + + } + print $fh "\n\n$client->{MSG}\n"; + print "\n"; + close $fh; +} -- cgit v1.2.1 From 4747bfbea3f4abb66d05a2bd1abed69e28a4aa3d Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 28 Jan 2018 02:41:17 -0600 Subject: RT# 73421 Fixed E-Mail pipeline to obey contact opt-in flags --- FS/FS/cust_main.pm | 96 ++++++++++++++++++++++------- FS/FS/cust_main_Mixin.pm | 38 +++++++++--- FS/FS/msg_template/email.pm | 29 ++++++++- httemplate/misc/email-customers.html | 27 ++++---- httemplate/view/cust_main/contacts_new.html | 2 +- 5 files changed, 146 insertions(+), 46 deletions(-) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 925eb4e44..b8d8f102f 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3022,48 +3022,101 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } -=item contact_list [ CLASSNUM, ... ] +=item contact_list [ CLASSNUM, DEST_FLAG... ] -Returns a list of contacts (L objects) for the customer. If -a list of contact classnums is given, returns only contacts in those -classes. If the pseudo-classnum 'invoice' is given, returns contacts that -are marked as invoice destinations. If '0' is given, also returns contacts -with no class. +Returns a list of contacts (L objects) for the customer. If no arguments are given, returns all contacts for the customer. +Arguments may contain classnums. When classnums are specified, only +contacts with a matching cust_contact.classnum are returned. When a +classnum of 0 is given, contacts with a null classnum are also included. + +Arguments may also contain the dest flag names 'invoice' or 'message'. +If given, contacts who's invoice_dest and/or invoice_message flags are +not set to 'Y' will be excluded. + =cut sub contact_list { my $self = shift; my $search = { table => 'contact', - select => 'contact.*, cust_contact.invoice_dest', + select => join(', ',( + 'contact.*', + 'cust_contact.invoice_dest', + 'cust_contact.message_dest', + )), addl_from => ' JOIN cust_contact USING (contactnum)', extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, }; - my @orwhere; + # Bugfix notes: + # Calling methods were relying on this method to use invoice_dest to + # block e-mail messages. Depending on parameters, this may or may not + # have actually happened. + # + # The bug could cause this SQL to be used to filter e-mail addresses: + # + # AND ( + # cust_contact.classnums IN (1,2,3) + # OR cust_contact.invoice_dest = 'Y' + # ) + # + # improperly including everybody with the opt-in flag AND everybody + # in the contact classes + # + # Possibility to introduce new bugs: + # If callers of this method called it incorrectly, and didn't notice + # because it seemed to send the e-mails they wanted. + + # WHERE ... + # AND ( + # ( cust_contact.classnum IN (1,2,3) ) + # OR + # ( cust_contact.classnum IS NULL ) + # + # AND ( + # ( cust_contact.invoice_dest = 'Y' ) + # OR + # ( cust_contact.message_dest = 'Y' ) + # ) + # ) + + my @and_dest; + my @or_classnum; my @classnums; - foreach (@_) { - if ( $_ eq 'invoice' ) { - push @orwhere, 'cust_contact.invoice_dest = \'Y\''; - } elsif ( $_ eq '0' ) { - push @orwhere, 'cust_contact.classnum is null'; + for (@_) { + if ($_ eq 'invoice' || $_ eq 'message') { + push @and_dest, " cust_contact.${_}_dest = 'Y' "; + } elsif ($_ eq '0') { + push @or_classnum, ' cust_contact.classnum IS NULL '; } elsif ( /^\d+$/ ) { push @classnums, $_; } else { - die "bad classnum argument '$_'"; + croak "bad classnum argument '$_'"; } } - if (@classnums) { - push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; - } - if (@orwhere) { - $search->{extra_sql} .= ' AND (' . - join(' OR ', map "( $_ )", @orwhere) . - ')'; + push @or_classnum, 'cust_contact.classnum IN ('.join(',',@classnums).')' + if @classnums; + + if (@or_classnum || @and_dest) { # catch, no arguments given + $search->{extra_sql} .= ' AND ( '; + + if (@or_classnum) { + $search->{extra_sql} .= join ' OR ', map {" ($_) "} @or_classnum; + $search->{extra_sql} .= ' AND ( ' if @and_dest; + } + + if (@and_dest) { + $search->{extra_sql} .= join ' OR ', map {" ($_) "} @and_dest; + $search->{extra_sql} .= ' ) ' if @or_classnum; + } + + $search->{extra_sql} .= ' ) '; + + warn "\$extra_sql: $search->{extra_sql} \n" if $DEBUG; } qsearch($search); @@ -5540,4 +5593,3 @@ L, L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 8b6569a74..169e1eb65 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -348,10 +348,21 @@ sub cust_search_sql { =item email_search_result HASHREF -Emails a notice to the specified customers. Customers without -invoice email destinations will be skipped. +Emails a notice to the specified customer's contact_email addresses. -Parameters: + +If the user has specified "Invoice recipients" on the send e-mail screen, +contact_email rows containing the invoice_dest flag will be included. +This option is default, if neither 'invoice' nor 'message' are present. + +If the user has specified "Message recipients" on the send e-mail screen, +contact_email rows containing the message_dest flag will be included. + +The selection is indicated by the presence of the text 'message' or +'invoice' within the to_contact_classnum argument. + + +Parameters: =over 4 @@ -386,9 +397,19 @@ Text body =item to_contact_classnum -The customer contact class (or classes, as a comma-separated list) to send -the message to. If unspecified, will be sent to any contacts that are marked -as invoice destinations (the equivalent of specifying 'invoice'). +This field contains a comma-separated list. This list may contain: + +- the text "invoice" indicating emails should only be sent to contact_email + addresses with the invoice_dest flag set +- the text "message" indicating emails should only be sent to contact_email + addresses with the message_dest flag set +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected =back @@ -483,8 +504,8 @@ sub email_search_result { next; # unlinked object; nothing else we can do } -my %to = {}; -if ($to) { $to{'to'} = $to; } + my %to = (); + if ($to) { $to{'to'} = $to; } my $cust_msg = $msg_template->prepare( 'cust_main' => $cust_main, @@ -736,4 +757,3 @@ L, L =cut 1; - diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 4ae89f056..753fd3d69 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -210,6 +210,22 @@ go away in the future. A L (or arrayref of them) to attach to the message. +=item to_contact_classnum + +Set a string containing a comma-separated list. This list may contain: + +- the text "invoice" indicating emails should only be sent to contact_email + addresses with the invoice_dest flag set +- the text "message" indicating emails should only be sent to contact_email + addresses with the message_dest flag set + - numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL + +If neither 'invoice' nor 'message' has been specified, this method will +behave as if 'invoice' had been selected + =cut =back @@ -296,8 +312,16 @@ sub prepare { my $classnum = $opt{'to_contact_classnum'} || ''; my @classes = ref($classnum) ? @$classnum : split(',', $classnum); - # traditional behavior: send to all invoice recipients - @classes = ('invoice') unless @classes; + + # There are two e-mail opt-in flags per contact_email address. + # If neither 'invoice' nor 'message' has been specified, default + # to 'invoice'. + # + # This default supports the legacy behavior of + # send to all invoice recipients + push @classes,'invoice' + unless grep {$_ eq 'invoice' || $_ eq 'message'} @classes; + @to = $cust_main->contact_list_email(@classes); # not guaranteed to produce contacts, but then customers aren't # guaranteed to have email addresses on file. in that case, env_to @@ -625,4 +649,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index fe637abe1..dc53f6d55 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -171,16 +171,18 @@ Template: Send to contacts:
- <& /elements/checkboxes.html, - 'style' => 'display: inline; vertical-align: top', - 'disable_links' => 1, - 'names_list' => \@contact_checkboxes, - 'element_name_prefix' => 'contact_class_', - 'checked_callback' => sub { - my($cgi, $name) = @_; - $name eq 'invoice' #others default to unchecked - }, - &> + <& /elements/checkboxes.html, + 'style' => 'display: inline; vertical-align: top', + 'disable_links' => 1, + 'names_list' => \@contact_checkboxes, + 'element_name_prefix' => 'contact_class_', + 'checked_callback' => sub { + # Called for each checkbox + # Return true to default as checked, false as unchecked + my($cgi, $name) = @_; + $name eq 'message' + }, + &>
% if ($send_to_domain) {
@@ -422,6 +424,8 @@ if ( !$cgi->param('preview') ) { push @contact_classnum, $1; if ( $1 eq 'invoice' ) { push @contact_classname, 'Invoice recipients'; + } elsif ( $1 eq 'message' ) { + push @contact_classname, 'Message recipients'; } else { my $contact_class = FS::contact_class->by_key($1); push @contact_classname, encode_entities($contact_class->classname); @@ -432,7 +436,8 @@ if ( !$cgi->param('preview') ) { } my @contact_checkboxes = ( - [ 'invoice' => { label => 'Invoice recipients' } ] + [ 'message' => { label => 'Message recipients' } ], + [ 'invoice' => { label => 'Invoice recipients' } ], ); foreach my $class (qsearch('contact_class', { disabled => '' })) { diff --git a/httemplate/view/cust_main/contacts_new.html b/httemplate/view/cust_main/contacts_new.html index a28b44934..fe412cc00 100644 --- a/httemplate/view/cust_main/contacts_new.html +++ b/httemplate/view/cust_main/contacts_new.html @@ -10,8 +10,8 @@ <%$th%>Contact <%$th%>Email <%$th%>Send invoices - <%$th%>Self-service <%$th%>Send messages + <%$th%>Self-service % foreach my $phone_type (@phone_type) { <%$th%><% $phone_type->typename |h %> % } -- cgit v1.2.1 From 280c44682a79e586af941e869e7a78ca8f367cf2 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sun, 28 Jan 2018 17:54:00 -0600 Subject: RT# 73421 Fix bug on some "Email customers" report links, docs --- FS/FS/cust_main.pm | 2 +- FS/FS/cust_main_Mixin.pm | 8 ++++---- FS/FS/msg_template/email.pm | 16 ++++++++-------- httemplate/search/cust_main.html | 8 +++++++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index b8d8f102f..a82d8a225 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3033,7 +3033,7 @@ contacts with a matching cust_contact.classnum are returned. When a classnum of 0 is given, contacts with a null classnum are also included. Arguments may also contain the dest flag names 'invoice' or 'message'. -If given, contacts who's invoice_dest and/or invoice_message flags are +If given, contacts who's invoice_dest and/or message_dest flags are not set to 'Y' will be excluded. =cut diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 169e1eb65..cceaa4bc7 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -399,10 +399,10 @@ Text body This field contains a comma-separated list. This list may contain: -- the text "invoice" indicating emails should only be sent to contact_email - addresses with the invoice_dest flag set -- the text "message" indicating emails should only be sent to contact_email - addresses with the message_dest flag set +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included - numbers representing classnum id values for email contact classes. If any classnum are present, emails should only be sent to contact_email addresses where contact_email.classnum contains one of these classes. diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 753fd3d69..37c1fab46 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -214,14 +214,14 @@ A L (or arrayref of them) to attach to the message. Set a string containing a comma-separated list. This list may contain: -- the text "invoice" indicating emails should only be sent to contact_email - addresses with the invoice_dest flag set -- the text "message" indicating emails should only be sent to contact_email - addresses with the message_dest flag set - - numbers representing classnum id values for email contact classes. - If any classnum are present, emails should only be sent to contact_email - addresses where contact_email.classnum contains one of these classes. - The classnum 0 also includes where contact_email.classnum IS NULL +- the text "invoice" indicating contacts with invoice_dest flag should + be included +- the text "message" indicating contacts with message_dest flag should + be included +- numbers representing classnum id values for email contact classes. + If any classnum are present, emails should only be sent to contact_email + addresses where contact_email.classnum contains one of these classes. + The classnum 0 also includes where contact_email.classnum IS NULL If neither 'invoice' nor 'message' has been specified, this method will behave as if 'invoice' had been selected diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 30162506f..0a43a82dd 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -140,8 +140,14 @@ my $menubar = []; if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) { + # URI::query_from does not support hashref + # results in: ...&contacts=HASH(0x55e16cb81da8)&... + my %query_hash = %search_hash; + delete $query_hash{contacts} + if exists $query_hash{contacts} && ref $query_hash{contacts}; + my $uri = new URI; - $uri->query_form( \%search_hash ); + $uri->query_form( \%query_hash ); my $query = $uri->query; push @$menubar, emt('Email a notice to these customers') => -- cgit v1.2.1 From e803d5f00368e951f7a4b82d5d390b53c4a6c827 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Tue, 30 Jan 2018 09:27:42 -0600 Subject: RT# 73422 Changes to report Customer Contacts --- FS/FS/cust_main.pm | 19 +++--- FS/FS/cust_main/Search.pm | 48 ++++++++++++-- .../elements/select-multiple-contact_class.html | 21 ++++++ .../elements/tr-select-multiple-contact_class.html | 32 +++++++++ httemplate/misc/email-customers.html | 75 +++++++++++++++++++--- httemplate/search/contact.html | 60 +++++++++++++++-- httemplate/search/report_contact.html | 5 ++ 7 files changed, 233 insertions(+), 27 deletions(-) create mode 100644 httemplate/elements/select-multiple-contact_class.html create mode 100644 httemplate/elements/tr-select-multiple-contact_class.html diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index a82d8a225..7c9868d7a 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3072,14 +3072,15 @@ sub contact_list { # WHERE ... # AND ( - # ( cust_contact.classnum IN (1,2,3) ) - # OR - # ( cust_contact.classnum IS NULL ) - # + # ( + # cust_contact.classnum IN (1,2,3) + # OR + # cust_contact.classnum IS NULL + # ) # AND ( - # ( cust_contact.invoice_dest = 'Y' ) + # cust_contact.invoice_dest = 'Y' # OR - # ( cust_contact.message_dest = 'Y' ) + # cust_contact.message_dest = 'Y' # ) # ) @@ -3105,12 +3106,14 @@ sub contact_list { $search->{extra_sql} .= ' AND ( '; if (@or_classnum) { - $search->{extra_sql} .= join ' OR ', map {" ($_) "} @or_classnum; + $search->{extra_sql} .= ' ( '; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum; + $search->{extra_sql} .= ' ) '; $search->{extra_sql} .= ' AND ( ' if @and_dest; } if (@and_dest) { - $search->{extra_sql} .= join ' OR ', map {" ($_) "} @and_dest; + $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest; $search->{extra_sql} .= ' ) ' if @or_classnum; } diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm index 2ec87cd14..815304bb4 100644 --- a/FS/FS/cust_main/Search.pm +++ b/FS/FS/cust_main/Search.pm @@ -1,6 +1,7 @@ package FS::cust_main::Search; use strict; +use Carp qw( croak ); use base qw( Exporter ); use vars qw( @EXPORT_OK $DEBUG $me $conf @fuzzyfields ); use String::Approx qw(amatch); @@ -804,15 +805,51 @@ sub search { unless $params->{'cancelled_pkgs'}; ## - # "with email address(es)" checkbox + # "with email address(es)" checkbox, + # also optionally: with_email_dest and with_contact_type ## - push @where, - 'EXISTS ( SELECT 1 FROM contact_email + if ($params->{with_email}) { + my @email_dest; + my $email_dest_sql; + my $contact_type_sql; + + if ($params->{with_email_dest}) { + croak unless ref $params->{with_email_dest} eq 'ARRAY'; + + @email_dest = @{$params->{with_email_dest}}; + $email_dest_sql = + " AND ( ". + join(' OR ',map(" cust_contact.${_}_dest IS NOT NULL ", @email_dest)). + " ) "; + # Can't use message_dist = 'Y' because single quotes are escaped later + } + if ($params->{with_contact_type}) { + croak unless ref $params->{with_contact_type} eq 'ARRAY'; + + my @contact_type = grep {/^\d+$/ && $_ > 0} @{$params->{with_contact_type}}; + my $has_null_type = 0; + $has_null_type = 1 if grep { $_ eq 0 } @{$params->{with_contact_type}}; + my $hnt_sql; + if ($has_null_type) { + $hnt_sql = ' OR ' if @contact_type; + $hnt_sql .= ' cust_contact.classnum IS NULL '; + } + + $contact_type_sql = + " AND ( ". + join(' OR ', map(" cust_contact.classnum = $_ ", @contact_type)). + $hnt_sql. + " ) "; + } + push @where, + "EXISTS ( SELECT 1 FROM contact_email JOIN cust_contact USING (contactnum) WHERE cust_contact.custnum = cust_main.custnum - )' - if $params->{'with_email'}; + $email_dest_sql + $contact_type_sql + ) "; + } ## # "with postal mail invoices" checkbox @@ -1390,4 +1427,3 @@ L, L =cut 1; - diff --git a/httemplate/elements/select-multiple-contact_class.html b/httemplate/elements/select-multiple-contact_class.html new file mode 100644 index 000000000..81a71cc25 --- /dev/null +++ b/httemplate/elements/select-multiple-contact_class.html @@ -0,0 +1,21 @@ +<%doc> + +Display a multi-select box containing all Email Types listed in +the contact_class table. + +NOTE: + Don't confuse "Contact Type" (contact_email.classnum) with + "Customer Class" (cust_main.classnum) + + +<% include( '/elements/select-table.html', + table => 'contact_class', + hashref => { disabled => '' }, + name_col => 'classname', + field => 'classnum', + pre_options => [ 0 => '(No Type)' ], + multiple => 1, + all_selected => 1, + @_, + ) +%> diff --git a/httemplate/elements/tr-select-multiple-contact_class.html b/httemplate/elements/tr-select-multiple-contact_class.html new file mode 100644 index 000000000..5de129324 --- /dev/null +++ b/httemplate/elements/tr-select-multiple-contact_class.html @@ -0,0 +1,32 @@ +<%doc> + + Displays Contact Types as a multi-select box. + + If no non-disabled Contact Types have been defined in contact_class table, + renders a hidden input field with a blank value. + + + +% if ($has_types) { + + <% $opt{'label'} || emt('Contact Type') %> + + <% include( '/elements/select-multiple-contact_class.html', %opt ) %> + + +% } else { + +% } + +<%init> + +my %opt = @_; +$opt{field} ||= $opt{element_name} ||= 'classnum'; + +my $has_types =()= qsearch({ + table => 'contact_class', + hashref => { disabled => '' }, + extra_sql => ' LIMIT 1 ', +}); + + diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index dc53f6d55..4520c7514 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -174,7 +174,7 @@ Template: <& /elements/checkboxes.html, 'style' => 'display: inline; vertical-align: top', 'disable_links' => 1, - 'names_list' => \@contact_checkboxes, + 'names_list' => \@optin_checkboxes, 'element_name_prefix' => 'contact_class_', 'checked_callback' => sub { # Called for each checkbox @@ -199,6 +199,27 @@ Template:
% } +% if (@active_classes) { + + +Contact Type: + +
+ <& /elements/checkboxes.html, + 'style' => 'display: inline; vertical-align: top', + 'disable_links' => 1, + 'names_list' => \@classnum_checkboxes, + 'element_name_prefix' => 'contact_class_', + 'checked_callback' => sub { + # Called for each checkbox + # Return true to default as checked, false as unchecked + my($cgi, $name) = @_; + exists $classnum_ischecked{$name}; + }, + &> +
+ +% }
@@ -342,6 +363,21 @@ if ( !$cgi->param('preview') ) { } else { + my @checked_email_dest; + my @checked_contact_type; + for ($cgi->param) { + if (/^contact_class_(.+)$/) { + my $f = $1; + if ($f eq 'invoice' || $f eq 'message') { + push @checked_email_dest, $f; + } elsif ( $f =~ /^\d+$/ ) { + push @checked_contact_type, $f; + } + } + } + $search{with_email_dest} = \@checked_email_dest if @checked_email_dest; + $search{with_contact_type} = \@checked_contact_type if @checked_contact_type; + my $sql_query = "FS::$table"->search(\%search); my $count_query = delete($sql_query->{'count_query'}); my $count_sth = dbh->prepare($count_query) @@ -391,6 +427,8 @@ if ( !$cgi->param('preview') ) { $sql_query->{'select'} = "$table.*"; $sql_query->{'order_by'} = ''; my $object = qsearchs($sql_query); + # Could use better error handling here... + die "No customers match the search criteria" unless ref $object; $cust = $object->cust_main; my %msgopts = ( 'cust_main' => $cust, @@ -435,16 +473,35 @@ if ( !$cgi->param('preview') ) { } } -my @contact_checkboxes = ( +# Build data structures for "Opt In" and "Contact Type" checkboxes +# +# By default, message recipients will be selected, this is a message. +# By default, all Contact Types will be selected, but this may be +# overridden by passing 'classnums' get/post values. If no contact +# types have been defined, the option will not be presented. + +my @active_classes = qsearch(contact_class => {disabled => ''} ); + +$CGI::LIST_CONTEXT_WARN = 0; +my @classnums = grep{ /^\d+$/ } $cgi->param('classnums'); + +my %classnum_ischecked; +if (@classnums) { + # values passed to form + $classnum_ischecked{$_} = 1 for @classnums; +} else { + # default values + $classnum_ischecked{$_->classnum} = 1 for @active_classes; + $classnum_ischecked{0} = 1; +} + +my @optin_checkboxes = ( [ 'message' => { label => 'Message recipients' } ], [ 'invoice' => { label => 'Invoice recipients' } ], ); - -foreach my $class (qsearch('contact_class', { disabled => '' })) { - push @contact_checkboxes, [ - $class->classnum, - { label => $class->classname } - ]; -} +my @classnum_checkboxes = ( + [ '0' => { label => '(None)' }], + map { [ $_->classnum => {label => $_->classname} ] } @active_classes, +); diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 5f02fef2f..9abbcfa1d 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -11,12 +11,19 @@ header => \@header, fields => \@fields, links => \@links, + html_init => $send_email_link, &> <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('List contacts'); +# Catch classnum values from multi-select box +# A classnum of 0 indicates to include rows where classnum IS NULL +$CGI::LIST_CONTEXT_WARN = 0; +my @classnum = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum'); +my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum'); + my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away push @select, map "contact.$_", qw( first last title ); my %hash = (); @@ -53,6 +60,18 @@ my $home_phone_sub = sub { join(', ', map $_->phonenum, @contact_homephone); }; +my $invoice_dest_sub = sub { + my $contact = shift; + my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum}); + $cust_contact->invoice_dest ? 'Y' : 'N'; +}; + +my $message_dest_sub = sub { + my $contact = shift; + my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum}); + $cust_contact->message_dest ? 'Y' : 'N'; +}; + my $link; #for closure in this sub, we'll define it later my $contact_classname_sub = sub { my $contact = shift; @@ -63,14 +82,14 @@ my $contact_classname_sub = sub { } elsif ( $link eq 'prospect_main' ) { $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } ); } else { - die 'guru meditation #5555'; + die "guru meditation #5555 (\$link: $link)"; } $X_contact->contact_classname; }; -my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type' ); -my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub ); -my @links = ( '', '', '', '', '', '', '', '', ); +my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type', 'Invoice Destination', 'Message Destination'); +my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub, $invoice_dest_sub, $message_dest_sub ); +my @links = ( '', '', '', '', '', '', '', '', '', ''); my $company_link = ''; @@ -93,6 +112,14 @@ if ( $link ) { ' LEFT JOIN cust_contact USING ( contactnum ) '. ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )'; $extra_sql = ' cust_contact.custnum IS NOT NULL '; + if (@classnum || $classnum_null) { + $extra_sql .= ' AND ( '; + $extra_sql .= ' cust_contact.classnum IN ('.join(',',@classnum).') ' + if @classnum; + $extra_sql .= ' OR ' if $classnum_null && @classnum; + $extra_sql .= ' cust_contact.classnum IS NULL ' if $classnum_null; + $extra_sql .= ' ) '; + } $company_link = [ $p.'view/cust_main.cgi?', 'custnum' ]; } elsif ( $link eq 'prospect_main' ) { push @header, 'Prospect'; @@ -103,6 +130,14 @@ if ( $link ) { ' LEFT JOIN prospect_contact USING ( contactnum ) '. ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )'; $extra_sql = ' prospect_contact.prospectnum IS NOT NULL '; + if (@classnum || $classnum_null) { + $extra_sql .= ' AND ( '; + $extra_sql .= ' prospect_contact.classnum IN ('.join(',',@classnum).') ' + if @classnum; + $extra_sql .= ' OR ' if $classnum_null && @classnum; + $extra_sql .= ' prospect_contact.classnum IS NULL ' if $classnum_null; + $extra_sql .= ' ) '; + } $company_link = [ $p.'view/prospect_main.html?', 'prospectnum' ]; } else { die "don't know how to report on contacts linked to specified table"; @@ -123,4 +158,21 @@ push @fields, 'comment'; $extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql if $extra_sql; +my $classnum_url_part; +if (@classnum) { + $classnum_url_part = join '', map{ "&classnums=$_" } @classnum; + $classnum_url_part .= '&classnums=0' if $classnum_null; +} +my $send_email_link = + "Email a notice to these customers"; + diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html index 3583bb428..ba91b4e7e 100644 --- a/httemplate/search/report_contact.html +++ b/httemplate/search/report_contact.html @@ -21,6 +21,11 @@ 'curr_value' => scalar( $cgi->param('link') ), &> + <& /elements/tr-select-multiple-contact_class.html, + label => 'Contact Type', + field => 'classnum', + &> + -- cgit v1.2.1 From c71fd54c5103b16e7f7296ce6cc6750986119bd8 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Thu, 1 Feb 2018 10:23:33 -0500 Subject: RT# 75095 - added comment --- FS/FS/cust_main/Import_Charges.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index f90e0496f..bff2ec2fb 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -68,6 +68,7 @@ sub batch_charge { if ( $format eq 'simple' ) { @fields = qw( custnum agent_custid amount pkg ); } elsif ( $format eq 'ooma' ) { + #below is gcet file. #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); -- cgit v1.2.1 From 42b6338522f6dc66771f49b38191b4201462fd82 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 2 Feb 2018 01:31:50 -0600 Subject: RT# 78592 CDR Import for Acme Packet VOIP Switches --- FS/FS/cdr.pm | 9 ++- FS/FS/cdr/acmepacket.pm | 157 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 FS/FS/cdr/acmepacket.pm diff --git a/FS/FS/cdr.pm b/FS/FS/cdr.pm index c4e9c47a3..331ac0f2f 100644 --- a/FS/FS/cdr.pm +++ b/FS/FS/cdr.pm @@ -1733,6 +1733,14 @@ sub _cdr_date_parse { # Telos 2014-10-10T05:30:33Z ($year, $mon, $day, $hour, $min, $sec) = ( $1, $2, $3, $4, $5, $6 ); $options{gmt} = 1; + } elsif ( $date =~ /^(\d+):(\d+):(\d+)\.\d+ \w+ (\w+) (\d+) (\d+)$/ ) { + ($hour, $min, $sec, $mon, $day, $year) = ( $1, $2, $3, $4, $5, $6 ); + $mon = { # Acme Packet: 15:54:56.868 PST DEC 18 2017 + # My best guess of month abbv they may use + JAN => '01', FEB => '02', MAR => '03', APR => '04', + MAY => '05', JUN => '06', JUL => '07', AUG => '08', + SEP => '09', OCT => '10', NOV => '11', DEC => '12' + }->{$mon}; } else { die "unparsable date: $date"; #maybe we shouldn't die... } @@ -1932,4 +1940,3 @@ L, schema.html from the base documentation. =cut 1; - diff --git a/FS/FS/cdr/acmepacket.pm b/FS/FS/cdr/acmepacket.pm new file mode 100644 index 000000000..e0e80cdd6 --- /dev/null +++ b/FS/FS/cdr/acmepacket.pm @@ -0,0 +1,157 @@ +package FS::cdr::acmepacket; + +=head1 NAME + +FS:cdr::acmepacket - CDR import definition based on Acme Packet Net-Net 4000 + +=head1 DESCRIPTION + +The Acme Packet NetNet 4000 S-CX6.4.0 generates an odd cdr log format: + + - Each row in the CSV may be in one of many various formats, some of + them undocumented. + - Columns are inconsistently enclosed with " characters + - Quoted column values may, themselves, contain unescaped quote marks. + This breaks Text::CSV (well technically the FORMAT is broken, not + Text::CSV). + - A single call can generate multiple CDR records. The only records we're + interested in are billable calls: + - These are called "Stop Record CSV Placement" in Acme Packet documentation + - These will always contain a "2" as the first column value + - These rows may be 175 or 269 fields in length. It's unclear if the + undocumented 269 column rows are an intentional Acme Packet format, or + a bug in their switch. The extra columns are inserted at idx 115, + and can safely be disregarded. + +NOTE ON DATE PARSING: + + The Acme Packet manual doesn't describe it's date format in detail. The sample + we were given contains only records from December. Dates are formatted like + so: 15:54:56.868 PST DEC 18 2017 + + I gave my best guess how they will format the month text in the parser + FS::cdr::_cdr_date_parse(). If this format doesn't import records on a + particular month, check there. + +=cut + +use strict; +use warnings; +use vars qw(%info); +use base qw(FS::cdr); +use FS::cdr qw(_cdr_date_parse); +use Text::CSV; + +my $DEBUG = 0; + +my $cbcsv = Text::CSV->new({binary => 1}) + or die "Error loading Text::CSV - ".Text::CSV->error_diag(); + +# Used to map source format into the contrived format created for import_fields +# $cdr_premap[ IMPORT_FIELDS_IDX ] = SOURCE_FORMAT_IDX +my @cdr_premap = ( + 9, # clid + 9, # src + 10, # dst + 22, # channel + 21, # dstchannel + 26, # src_ip_addr + 28, # dst_ip_addr + 13, # startdate + 14, # answerdate + 15, # enddate + 12, # duration + 12, # billsec + 3, # userfield +); + +our %info = ( + name => 'Acme Packet', + weight => 600, + header => 0, + type => 'csv', + + import_fields => [ + # freeside # [idx] acmepacket + 'clid', # 9 Calling-Station-Id + 'src', # 9 Calling-Station-Id + 'dst', # 10 Called-Station-Id + 'channel', # 22 Acme-Session-Ingress-Realm + 'dstchannel', # 23 Acme-Session-Egress-Realm + 'src_ip_addr', # 26 Acme-Flow-In-Src-Adr_FS1_f + 'dst_ip_addr', # 28 Acme-Flow-In-Dst-Addr_FS1_f + 'startdate', # 13 h323-setup-time + 'answerdate', # 14 h323-connect-time + 'enddate', # 15 h323-disconnect-time + 'duration', # 12 Acct-Session-Time + 'billsec', # 12 Acct-Session-Time + 'userfield', # 3 Acct-Session-Id + ], + + row_callback => sub { + my $line = shift; + + # Only process records whose first column contains a 2 + return undef unless $line =~ /^2\,/; + + # Replace unescaped quote characters within quote-enclosed text cols + $line =~ s/(?parse($line) ) { + warn "Text::CSV Error parsing Acme Packet CDR: ".$cbcsv->error_diag(); + return undef; + } + + my @row = $cbcsv->fields(); + if (@row == 269) { + # Splice out the extra columns + @row = (@row[0..114], @row[209..@row-1]); + } elsif (@row != 175) { + warn "Unknown row format parsing Acme Packet CDR"; + return undef; + } + + my @out = map{ $row[$_] } @cdr_premap; + + if ($DEBUG) { + warn "~~~~~~~~~~pre-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # answerdate, enddate, startdate + for (7,8,9) { + $out[$_] = _cdr_date_parse($out[$_]); + if ($out[$_] =~ /\D/) { + warn "Unparsable date in Acme Packet CDR: ($out[$_])"; + return undef; + } + } + + # clid, dst, src CDR field formatted as one of the following: + # 'WIRELESS CALLER';tag=SDepng302 + # ;tag=SDepng302 + + # clid + $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : ""; + + # src, dst + # All of the sample data given shows sip connections. In case the same + # switch is hooked into another circuit type in the future, we'll just + # tease out a length 7+ number not contained in the caller-id-text field + for (1,2) { + $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion + $out[$_] = $out[$_] =~ /(\d{7,})/ ? $1 : undef; + } + + if ($DEBUG) { + warn "~~~~~~~~~~post-processed~~~~~~~~~~~~~~~~ \n"; + warn "$_ $out[$_] \n" for (0..@out-1); + } + + # I haven't seen commas in sample data text fields. Extra caution, + # mangle commas into periods as we pass back to importer + join ',', map{ $_ =~ s/\,/\./g; $_ } @out; + }, +); + +1; -- cgit v1.2.1 From cbb6450c25690e5b3fccc6cfddd497de45f062d5 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Fri, 2 Feb 2018 03:47:41 -0600 Subject: RT# 78952 CDR Import for Acme Packet Improved caller-id parsing --- FS/FS/cdr/acmepacket.pm | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/FS/FS/cdr/acmepacket.pm b/FS/FS/cdr/acmepacket.pm index e0e80cdd6..1f00e4086 100644 --- a/FS/FS/cdr/acmepacket.pm +++ b/FS/FS/cdr/acmepacket.pm @@ -130,17 +130,28 @@ our %info = ( # clid, dst, src CDR field formatted as one of the following: # 'WIRELESS CALLER';tag=SDepng302 # ;tag=SDepng302 + # '12513001300';tag=SDepng302 # clid $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : ""; # src, dst - # All of the sample data given shows sip connections. In case the same - # switch is hooked into another circuit type in the future, we'll just - # tease out a length 7+ number not contained in the caller-id-text field + # Use the 7+ digit number as the src/dst phone number. + # Prefer using the number within label. If there is not one, + # allow using number from caller-id text field. for (1,2) { + my $f = $out[$_]; $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion - $out[$_] = $out[$_] =~ /(\d{7,})/ ? $1 : undef; + if ($out[$_] =~ /(\d{7,})/) { + # Using number from + $out[$_] = $1; + } elsif ($f =~ /(\d{7,})/) { + # Using number from caller-id text + $out[$_] = $1; + } else { + # No phone number, perhaps an IP only call + $out[$_] = undef; + } } if ($DEBUG) { -- cgit v1.2.1 From 560af69840e7e3fcebf65d5d56ca4efc0851efec Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 3 Feb 2018 20:16:47 -0600 Subject: RT# 73422 Fix report Customer Contacts --- httemplate/elements/tr-checkbox-multiple.html | 20 ++ httemplate/misc/email-customers.html | 29 +- httemplate/search/contact.html | 372 ++++++++++++++++---------- httemplate/search/report_contact.html | 32 ++- 4 files changed, 297 insertions(+), 156 deletions(-) diff --git a/httemplate/elements/tr-checkbox-multiple.html b/httemplate/elements/tr-checkbox-multiple.html index 4d754b007..baf18f916 100644 --- a/httemplate/elements/tr-checkbox-multiple.html +++ b/httemplate/elements/tr-checkbox-multiple.html @@ -1,3 +1,23 @@ +<%doc> + +Display a containing multiple checkboxes + +USAGE: + +<& /elements/tr-checkbox-multipe.html, + label => emt('Label'), + field => 'field_name', + options => ['opt1', 'opt2'], + labels => { + opt1 => 'Option 1', + opt2 => 'Option 2', + }, + value => { + opt2 => '1', # opt2 defaults as checked + } +&> + + <% include('tr-td-label.html', @_ ) %> > diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 4520c7514..f52c6b36a 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -180,7 +180,7 @@ Template: # Called for each checkbox # Return true to default as checked, false as unchecked my($cgi, $name) = @_; - $name eq 'message' + exists $dest_ischecked{$name}; }, &> @@ -466,7 +466,9 @@ if ( !$cgi->param('preview') ) { push @contact_classname, 'Message recipients'; } else { my $contact_class = FS::contact_class->by_key($1); - push @contact_classname, encode_entities($contact_class->classname); + push @contact_classname, encode_entities( + $contact_class ? $contact_class->classname : '(none)' + ); } } } @@ -482,19 +484,30 @@ if ( !$cgi->param('preview') ) { my @active_classes = qsearch(contact_class => {disabled => ''} ); +my %classnum_ischecked; +my %dest_ischecked; + $CGI::LIST_CONTEXT_WARN = 0; -my @classnums = grep{ /^\d+$/ } $cgi->param('classnums'); +if ( my @in_classnums = $cgi->param('classnums') ) { + # Set checked boxes from form input + for my $v (@in_classnums) { + + if ( $v =~ /^\d+$/ ) { + $classnum_ischecked{$v} = 1 + } elsif ( $v =~ /^(invoice|message)$/ ) { + $dest_ischecked{$v} = 1; + } -my %classnum_ischecked; -if (@classnums) { - # values passed to form - $classnum_ischecked{$_} = 1 for @classnums; + } } else { - # default values + # Checked boxes default values $classnum_ischecked{$_->classnum} = 1 for @active_classes; $classnum_ischecked{0} = 1; } +# At least one destination is required +$dest_ischecked{message} = 1 unless %dest_ischecked; + my @optin_checkboxes = ( [ 'message' => { label => 'Message recipients' } ], [ 'invoice' => { label => 'Invoice recipients' } ], diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index 9abbcfa1d..e02833319 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -1,178 +1,272 @@ <& elements/search.html, - title => 'Contacts', + title => emt('Contacts'), name_singular => 'contact', - query => { select => join(', ', @select), - table => 'contact', - addl_from => $addl_from, - hashref => \%hash, - extra_sql => $extra_sql, - }, - count_query => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX - header => \@header, - fields => \@fields, - links => \@links, - html_init => $send_email_link, + query => { + select => join(', ', @select), + table => 'contact', + addl_from => $addl_from, + hashref => {}, #\%hash, + extra_sql => "WHERE $extra_sql", + order_by => "ORDER BY contact_last,contact_first,contact_email_emailaddress" + }, + count_query => " + SELECT COUNT(*) + FROM contact + $addl_from + WHERE $extra_sql + ", + header => \@header, + fields => \@fields, + links => \@links, + html_init => $send_email_link, +# agent_virt => 1, # Not supported unless table is cust_main/prospect_main &> <%init> die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('List contacts'); +my $DEBUG = 0; + # Catch classnum values from multi-select box # A classnum of 0 indicates to include rows where classnum IS NULL $CGI::LIST_CONTEXT_WARN = 0; my @classnum = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum'); my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum'); -my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away -push @select, map "contact.$_", qw( first last title ); -my %hash = (); -my $addl_from = ''; +# Catch destination values from dest multi-checkbox, default to message +# irrelevant to prospect contacts +my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest'); +@dest = ('message') unless @dest; -my $email_sub = sub { - my $contact = shift; - #can't because contactnum is in the wrong field #my @contact_email = $contact->contact_email; - my @contact_email = qsearch('contact_email', { 'contactnum' => $contact->contact_contactnum } ); - join(', ', map $_->emailaddress, @contact_email); -}; -my $work_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Work' }); - #can't because contactnum is in the wrong field - my @contact_workphone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_workphone); -}; +# Cache the contact_class table +my %classname = + map {$_->classnum => $_->classname} + qsearch(contact_class => {disabled => ''}); -my $mobile_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Mobile' }); - #can't because contactnum is in the wrong field - my @contact_mobilephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_mobilephone); -}; +# This data structure is used to generate the sql query parameters +my %colmap = ( + # These are included regardless of which tables we're viewing + common => { + cols => { + contact => [qw/first last title contactnum/], + contact_email => [qw/emailaddress/], + }, + joinsql => " + LEFT JOIN contact_email + ON (contact.contactnum = contact_email.contactnum) + ", + }, -my $home_phone_sub = sub { - my $contact = shift; - my $phone_type = qsearchs('phone_type', { 'typename' => 'Home' }); - #can't because contactnum is in the wrong field - my @contact_homephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } ); - join(', ', map $_->phonenum, @contact_homephone); -}; + # These are included if we're viewing customer records + customer => { + cols => { + cust_main => [qw/first last company/], + cust_contact => [qw/ + custnum classnum invoice_dest message_dest selfservice_access comment + /], + }, + joinsql => " + LEFT JOIN cust_contact + ON (contact.contactnum = cust_contact.contactnum) + LEFT JOIN cust_main + ON (cust_contact.custnum = cust_main.custnum) + ", + }, -my $invoice_dest_sub = sub { - my $contact = shift; - my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum}); - $cust_contact->invoice_dest ? 'Y' : 'N'; -}; + # These are included if we're viewing prospect records + prospect => { + cols => { + prospect_main => [qw/company/], + prospect_contact => [qw/prospectnum classnum comment/], + }, + joinsql => " + LEFT JOIN prospect_contact + ON (contact.contactnum = prospect_contact.contactnum) + LEFT JOIN prospect_main + ON (prospect_contact.prospectnum = prospect_main.prospectnum) + ", + }, +); -my $message_dest_sub = sub { - my $contact = shift; - my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum}); - $cust_contact->message_dest ? 'Y' : 'N'; -}; +my @select; +my $addl_from; +my $extra_sql; +my $hashref; +my $link = $cgi->param('link'); # cust_main, prospect_main or both + +my @rectypes = ('common'); +push @rectypes,'customer' if $link eq 'cust_main' || $link eq 'both'; +push @rectypes,'prospect' if $link eq 'prospect_main' || $link eq 'both'; -my $link; #for closure in this sub, we'll define it later -my $contact_classname_sub = sub { - my $contact = shift; - my %hash = ( 'contactnum' => $contact->contact_contactnum ); - my $X_contact; - if ( $link eq 'cust_main' ) { - $X_contact = qsearchs('cust_contact', { %hash, 'custnum' => $contact->custnum } ); - } elsif ( $link eq 'prospect_main' ) { - $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } ); - } else { - die "guru meditation #5555 (\$link: $link)"; +# Build @select and $addl_from +for my $key (@rectypes) { + $addl_from .= $colmap{$key}->{joinsql}; + my $cols = $colmap{$key}->{cols}; + for my $tbl (keys %{$cols}) { + push @select, map{ "$tbl.$_ AS ${tbl}_$_" } @{$cols->{$tbl}}; } - $X_contact->contact_classname; -}; +} -my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type', 'Invoice Destination', 'Message Destination'); -my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub, $invoice_dest_sub, $message_dest_sub ); -my @links = ( '', '', '', '', '', '', '', '', '', ''); +# Filter for custnum/prospectnum +$extra_sql .= ' ('; +$extra_sql .= "cust_contact.custnum IS NOT NULL" + if $link eq 'cust_main' || $link eq 'both'; +$extra_sql .= " OR " if $link eq 'both'; +$extra_sql .= "prospect_contact.prospectnum IS NOT NULL" + if $link eq 'prospect_main' || $link eq 'both'; +$extra_sql .= ') '; -my $company_link = ''; +# Filter for Contact Type +if (@classnum || $classnum_null) { + my @stm; -if ( $cgi->param('selfservice_access') eq 'Y' ) { - $hash{'selfservice_access'} = 'Y'; -} + push @stm, 'cust_contact.classnum IN ('.join(',',@classnum).')' + if @classnum && ($link eq 'cust_main' || $link eq 'both'); -my $extra_sql = ''; -$link = $cgi->param('link'); -if ( $link ) { - - my $as = ') AS prospect_or_customer'; - - if ( $link eq 'cust_main' ) { - push @header, 'Customer'; - push @select, - "COALESCE( cust_main.company, cust_main.first||' '||cust_main.last $as", - map "cust_contact.$_", qw( custnum classnum comment selfservice_access ); - $addl_from = - ' LEFT JOIN cust_contact USING ( contactnum ) '. - ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )'; - $extra_sql = ' cust_contact.custnum IS NOT NULL '; - if (@classnum || $classnum_null) { - $extra_sql .= ' AND ( '; - $extra_sql .= ' cust_contact.classnum IN ('.join(',',@classnum).') ' - if @classnum; - $extra_sql .= ' OR ' if $classnum_null && @classnum; - $extra_sql .= ' cust_contact.classnum IS NULL ' if $classnum_null; - $extra_sql .= ' ) '; - } - $company_link = [ $p.'view/cust_main.cgi?', 'custnum' ]; - } elsif ( $link eq 'prospect_main' ) { - push @header, 'Prospect'; - push @select, - "COALESCE( prospect_main.company, contact.first||' '||contact.last $as", - map "prospect_contact.$_", qw( prospectnum classnum comment ); - $addl_from = - ' LEFT JOIN prospect_contact USING ( contactnum ) '. - ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )'; - $extra_sql = ' prospect_contact.prospectnum IS NOT NULL '; - if (@classnum || $classnum_null) { - $extra_sql .= ' AND ( '; - $extra_sql .= ' prospect_contact.classnum IN ('.join(',',@classnum).') ' - if @classnum; - $extra_sql .= ' OR ' if $classnum_null && @classnum; - $extra_sql .= ' prospect_contact.classnum IS NULL ' if $classnum_null; - $extra_sql .= ' ) '; - } - $company_link = [ $p.'view/prospect_main.html?', 'prospectnum' ]; - } else { - die "don't know how to report on contacts linked to specified table"; - } + push @stm, 'prospect_contact.classnum IN ('.join(',',@classnum).')' + if @classnum && ($link eq 'prospect_main' || $link eq 'both'); + + push @stm, 'cust_contact.classnum IS NULL' + if $classnum_null && ($link eq 'cust_main' || $link eq 'both'); + + push @stm, 'prospect_contact.classnum IS NULL' + if $classnum_null && ($link eq 'prospect_main' || $link eq 'both'); - #because right now its harder to show it for both kinds of contacts - push @fields, 'prospect_or_customer'; - push @links, $company_link; + $extra_sql .= "\nAND (" . join(' OR ',@stm) . ') '; +} +# Filter for destination +if (@dest && ($link eq 'cust_main' || $link eq 'both')) { + my @stm; + push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest; + push @stm, "prospect_contact.prospectnum IS NOT NULL" if $link eq 'both'; + $extra_sql .= "\nAND (".join(' OR ',@stm).') '; } -push @header, 'Self-service'; -push @fields, 'selfservice_access'; +if ($DEBUG) { + print "
\n";
+  print "select \n";
+  print join ",\n",@select;
+  print "\n";
+  print "from contact \n";
+  print "$addl_from\n";
+  print "WHERE \n $extra_sql\n";
+  print "
\n"; +} -push @header, 'Comment'; -push @fields, 'comment'; +# Prepare to display phone numbers +my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/); +my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/); +my $get_phone_sub = sub { + my $type = shift; + return sub { + my $rec = shift; + my @p = qsearch('contact_phone', { + contactnum => $rec->contact_contactnum, + phonetypenum => $phoneid{$type} + }); + @p ? (join ', ',map{$_->phonenum} @p) : undef; + }; +}; -$extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql - if $extra_sql; +# Cache contact types +my %classname = + map {$_->classnum => $_->classname} + qsearch(contact_class => {disabled => ''}); + +# And now for something completly different: +my @report = ( + { label => 'First', field => sub { shift->contact_first }}, + { label => 'Last', field => sub { shift->contact_last }}, + { label => 'Title', field => sub { shift->contact_title }}, + { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }}, + { label => 'Work Phone', field => $get_phone_sub->('Work') }, + { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') }, + { label => 'Home Phone', field => $get_phone_sub->('Home') }, + { label => 'Type', + field => sub { + my $rec = shift; + if ($rec->cust_contact_custnum) { + return $rec->cust_contact_classnum + ? $classname{$rec->cust_contact_classnum} + : undef; + } else { + return $rec->prospect_contact_classnum + ? $classname{$rec->prospect_contact_classnum} + : undef; + } + }}, + { label => 'Send Invoices', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_invoice_dest ? 'Y' : 'N'; + }}, + { label => 'Send Messages', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_message_dest ? 'Y' : 'N'; + }}, + { label => 'Customer', + link => sub { + my $rec = shift; + $rec->prospect_contact_prospectnum + ? ["${p}view/prospect_main.html?", 'prospect_contact_prospectnum' ] + : ["${p}view/cust_main.cgi?", 'cust_contact_custnum' ]; + }, + field => sub { + my $rec = shift; + if ($rec->prospect_contact_prospectnum) { + return $rec->contact_company + || $rec->contact_last.' '.$rec->contact_first; + } + $rec->cust_main_company || $rec->cust_main_last.' '.$rec->cust_main_first; + }}, + { label => 'Self-service', + field => sub { + my $rec = shift; + return 'N/A' if $rec->prospect_contact_prospectnum; + $rec->cust_contact_selfservice_access ? 'Y' : 'N'; + }}, + { label => 'Comment', + field => sub { + my $rec = shift; + $rec->prospect_contact_prospectnum + ? $rec->prospect_contact_comment + : $rec->cust_contact_comment; + }}, +); + +my (@header, @fields, @links); +for my $col (@report) { + push @header, emt($col->{label}); + push @fields, $col->{field}; + push @links, ($col->{link} || ""); +} my $classnum_url_part; if (@classnum) { - $classnum_url_part = join '', map{ "&classnums=$_" } @classnum; + $classnum_url_part = join '', map{ "&classnums=$_" } @classnum, @dest; $classnum_url_part .= '&classnums=0' if $classnum_null; } -my $send_email_link = - "Email a notice to these customers"; + +# E-mail pipeline doesn't support mixing prospects and customers in one go +my $send_email_link = undef; +if ($link eq 'cust_main') { + $send_email_link = + "Email a notice to these customers"; +} diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html index ba91b4e7e..309f11e96 100644 --- a/httemplate/search/report_contact.html +++ b/httemplate/search/report_contact.html @@ -4,25 +4,39 @@ - <& /elements/tr-select-agent.html, - 'curr_value' => scalar( $cgi->param('agentnum') ), - 'label' => emt('Contacts for agent: '), - 'disable_empty' => 0, - &> +%# This has never been actually supported on this report. +%# Remove the selectbox until support is implemented +%# +%# <& /elements/tr-select-agent.html, +%# 'curr_value' => scalar( $cgi->param('agentnum') ), +%# 'label' => emt('Contacts for agent: '), +%# 'disable_empty' => 0, +%# &> <& /elements/tr-select.html, - 'label' => 'Contact source', #??? not "type" - contacts have a type + 'label' => 'Contact source:', #??? not "type" - contacts have a type 'field' => 'link', - 'options' => [ 'prospect_main', 'cust_main', '' ], + 'options' => [ 'prospect_main', 'cust_main', 'both' ], 'labels' => { 'prospect_main' => 'Prospect contacts', 'cust_main' => 'Customer contacts', - '' => 'All contacts', + 'both' => 'All contacts', }, 'curr_value' => scalar( $cgi->param('link') ), &> + <& /elements/tr-checkbox-multiple.html, + label => emt('Destinations').':', + field => 'dest', + options => [ 'message', 'invoice' ], + labels => { + invoice => 'Invoice recipients', + message => 'Message recipients', + }, + value => { message => 1 }, + &> + <& /elements/tr-select-multiple-contact_class.html, - label => 'Contact Type', + label => emt('Contact Type').':', field => 'classnum', &> -- cgit v1.2.1 From 633ca08b978e5c9fa061d24ee2a2f78918e218de Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Sat, 3 Feb 2018 22:19:13 -0600 Subject: RT# 73422 Agent virt update for report Customer Contacts --- httemplate/search/contact.html | 100 ++++++++++++++------------------- httemplate/search/elements/search.html | 6 +- httemplate/search/report_contact.html | 20 +++---- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html index e02833319..aaa591cf4 100644 --- a/httemplate/search/contact.html +++ b/httemplate/search/contact.html @@ -3,15 +3,15 @@ name_singular => 'contact', query => { select => join(', ', @select), - table => 'contact', + table => $link, addl_from => $addl_from, - hashref => {}, #\%hash, + hashref => {}, extra_sql => "WHERE $extra_sql", order_by => "ORDER BY contact_last,contact_first,contact_email_emailaddress" }, count_query => " SELECT COUNT(*) - FROM contact + FROM $link $addl_from WHERE $extra_sql ", @@ -19,7 +19,8 @@ fields => \@fields, links => \@links, html_init => $send_email_link, -# agent_virt => 1, # Not supported unless table is cust_main/prospect_main + agent_virt => 1, + agent_pos => 11, &> <%init> @@ -39,7 +40,6 @@ my $classnum_null = grep{ $_ eq 0 } $cgi->param('classnum'); my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest'); @dest = ('message') unless @dest; - # Cache the contact_class table my %classname = map {$_->classnum => $_->classname} @@ -53,14 +53,11 @@ my %colmap = ( contact => [qw/first last title contactnum/], contact_email => [qw/emailaddress/], }, - joinsql => " - LEFT JOIN contact_email - ON (contact.contactnum = contact_email.contactnum) - ", + joinsql => "", }, # These are included if we're viewing customer records - customer => { + cust_main => { cols => { cust_main => [qw/first last company/], cust_contact => [qw/ @@ -69,23 +66,27 @@ my %colmap = ( }, joinsql => " LEFT JOIN cust_contact - ON (contact.contactnum = cust_contact.contactnum) - LEFT JOIN cust_main - ON (cust_contact.custnum = cust_main.custnum) + ON (cust_main.custnum = cust_contact.custnum) + LEFT JOIN contact + on (cust_contact.contactnum = contact.contactnum) + LEFT JOIN contact_email + ON (cust_contact.contactnum = contact_email.contactnum) ", }, # These are included if we're viewing prospect records - prospect => { + prospect_main => { cols => { prospect_main => [qw/company/], prospect_contact => [qw/prospectnum classnum comment/], }, joinsql => " LEFT JOIN prospect_contact - ON (contact.contactnum = prospect_contact.contactnum) - LEFT JOIN prospect_main - ON (prospect_contact.prospectnum = prospect_main.prospectnum) + ON (prospect_main.prospectnum = prospect_contact.prospectnum) + LEFT JOIN contact + on (prospect_contact.contactnum = contact.contactnum) + LEFT JOIN contact_email + ON (prospect_contact.contactnum = contact_email.contactnum) ", }, ); @@ -94,14 +95,16 @@ my @select; my $addl_from; my $extra_sql; my $hashref; -my $link = $cgi->param('link'); # cust_main, prospect_main or both +my $link = $cgi->param('link'); # cust_main or prospect_main + +push @select,'agentnum'; -my @rectypes = ('common'); -push @rectypes,'customer' if $link eq 'cust_main' || $link eq 'both'; -push @rectypes,'prospect' if $link eq 'prospect_main' || $link eq 'both'; +# this shouldn't happen without funny-busines +die "Invalid \$link type ($link)" + unless $link eq 'cust_main' || $link eq 'prospect_main'; # Build @select and $addl_from -for my $key (@rectypes) { +for my $key ('common', $link) { $addl_from .= $colmap{$key}->{joinsql}; my $cols = $colmap{$key}->{cols}; for my $tbl (keys %{$cols}) { @@ -109,39 +112,19 @@ for my $key (@rectypes) { } } -# Filter for custnum/prospectnum -$extra_sql .= ' ('; -$extra_sql .= "cust_contact.custnum IS NOT NULL" - if $link eq 'cust_main' || $link eq 'both'; -$extra_sql .= " OR " if $link eq 'both'; -$extra_sql .= "prospect_contact.prospectnum IS NOT NULL" - if $link eq 'prospect_main' || $link eq 'both'; -$extra_sql .= ') '; - # Filter for Contact Type if (@classnum || $classnum_null) { my @stm; - - push @stm, 'cust_contact.classnum IN ('.join(',',@classnum).')' - if @classnum && ($link eq 'cust_main' || $link eq 'both'); - - push @stm, 'prospect_contact.classnum IN ('.join(',',@classnum).')' - if @classnum && ($link eq 'prospect_main' || $link eq 'both'); - - push @stm, 'cust_contact.classnum IS NULL' - if $classnum_null && ($link eq 'cust_main' || $link eq 'both'); - - push @stm, 'prospect_contact.classnum IS NULL' - if $classnum_null && ($link eq 'prospect_main' || $link eq 'both'); - - $extra_sql .= "\nAND (" . join(' OR ',@stm) . ') '; + my $tbl = $link eq 'cust_main' ? 'cust_contact' : 'prospect_contact'; + push @stm, "${tbl}.classnum IN (".join(',',@classnum).')' if @classnum; + push @stm, "${tbl}.classnum IS NULL" if $classnum_null; + $extra_sql .= " (" . join(' OR ',@stm) . ') '; } # Filter for destination -if (@dest && ($link eq 'cust_main' || $link eq 'both')) { +if (@dest && $link eq 'cust_main') { my @stm; push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest; - push @stm, "prospect_contact.prospectnum IS NOT NULL" if $link eq 'both'; $extra_sql .= "\nAND (".join(' OR ',@stm).') '; } @@ -150,13 +133,14 @@ if ($DEBUG) { print "select \n"; print join ",\n",@select; print "\n"; - print "from contact \n"; + print "from $link \n"; print "$addl_from\n"; print "WHERE \n $extra_sql\n"; print "\n"; } # Prepare to display phone numbers +# adds 3 additional queries per table record :-( my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/); my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/); my $get_phone_sub = sub { @@ -178,13 +162,13 @@ my %classname = # And now for something completly different: my @report = ( - { label => 'First', field => sub { shift->contact_first }}, - { label => 'Last', field => sub { shift->contact_last }}, - { label => 'Title', field => sub { shift->contact_title }}, + { label => 'First', field => sub { shift->contact_first }}, + { label => 'Last', field => sub { shift->contact_last }}, + { label => 'Title', field => sub { shift->contact_title }}, { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }}, - { label => 'Work Phone', field => $get_phone_sub->('Work') }, + { label => 'Work Phone', field => $get_phone_sub->('Work') }, { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') }, - { label => 'Home Phone', field => $get_phone_sub->('Home') }, + { label => 'Home Phone', field => $get_phone_sub->('Home') }, { label => 'Type', field => sub { my $rec = shift; @@ -213,9 +197,9 @@ my @report = ( { label => 'Customer', link => sub { my $rec = shift; - $rec->prospect_contact_prospectnum - ? ["${p}view/prospect_main.html?", 'prospect_contact_prospectnum' ] - : ["${p}view/cust_main.cgi?", 'cust_contact_custnum' ]; + $rec->cust_main_custnum + ? ["${p}view/cust_main.cgi?", 'cust_main_custnum' ] + : ["${p}view/prospect_main.html?", 'prospect_main_prospectnum' ]; }, field => sub { my $rec = shift; @@ -253,12 +237,14 @@ if (@classnum) { $classnum_url_part .= '&classnums=0' if $classnum_null; } -# E-mail pipeline doesn't support mixing prospects and customers in one go +# E-mail pipeline, from email-customers.html through to email queue job, +# doesn't support cust_prospect table my $send_email_link = undef; if ($link eq 'cust_main') { $send_email_link = " 1, # set true if this search should be # agent-virtualized 'agent_null' => 1, # set true to view global records always diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html index 309f11e96..048fefb7a 100644 --- a/httemplate/search/report_contact.html +++ b/httemplate/search/report_contact.html @@ -4,22 +4,20 @@
-%# This has never been actually supported on this report. -%# Remove the selectbox until support is implemented -%# -%# <& /elements/tr-select-agent.html, -%# 'curr_value' => scalar( $cgi->param('agentnum') ), -%# 'label' => emt('Contacts for agent: '), -%# 'disable_empty' => 0, -%# &> + <& /elements/tr-select-agent.html, + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'label' => emt('Contacts for agent: '), + 'disable_empty' => 0, + &> +% # Selecting contacts and prospects at the same time has been sacrificed +% # for agent virtualization <& /elements/tr-select.html, - 'label' => 'Contact source:', #??? not "type" - contacts have a type + 'label' => 'Contact source:', 'field' => 'link', - 'options' => [ 'prospect_main', 'cust_main', 'both' ], + 'options' => [ 'prospect_main', 'cust_main' ], 'labels' => { 'prospect_main' => 'Prospect contacts', 'cust_main' => 'Customer contacts', - 'both' => 'All contacts', }, 'curr_value' => scalar( $cgi->param('link') ), &> -- cgit v1.2.1 From 15d596e3090f3bde642917b56563736cd1ee2e90 Mon Sep 17 00:00:00 2001 From: Christopher Burger Date: Tue, 6 Feb 2018 10:36:31 -0500 Subject: RT# 75095 - fixed ooma format to import all charges, and unified import process to allow new formats to be added quickly. consistant with cdr imports --- FS/FS/cust_main/Import_Charges.pm | 60 +++++++++++++++++++++------- FS/FS/cust_main/import_charges/gcet.pm | 26 ++++++++++++ FS/FS/cust_main/import_charges/ooma.pm | 21 ++++++++++ FS/FS/cust_main/import_charges/simple.pm | 21 ++++++++++ httemplate/misc/cust_main-import_charges.cgi | 8 ++-- 5 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 FS/FS/cust_main/import_charges/gcet.pm create mode 100644 FS/FS/cust_main/import_charges/ooma.pm create mode 100644 FS/FS/cust_main/import_charges/simple.pm diff --git a/FS/FS/cust_main/Import_Charges.pm b/FS/FS/cust_main/Import_Charges.pm index bff2ec2fb..3d2031d45 100644 --- a/FS/FS/cust_main/Import_Charges.pm +++ b/FS/FS/cust_main/Import_Charges.pm @@ -8,6 +8,48 @@ use FS::UID qw( dbh ); use FS::CurrentUser; use FS::Record qw( qsearchs ); use FS::cust_main; +use FS::Conf; + +my $DEBUG = ''; + +my %import_charges_info; +foreach my $INC ( @INC ) { + warn "globbing $INC/FS/cust_main/import_charges/[a-z]*.pm\n" if $DEBUG; + foreach my $file ( glob("$INC/FS/cust_main/import_charges/[a-z]*.pm") ) { + warn "attempting to load import charges format info from $file\n" if $DEBUG; + $file =~ /\/(\w+)\.pm$/ or do { + warn "unrecognized file in $INC/FS/cust_main/import_charges/: $file\n"; + next; + }; + my $mod = $1; + my $info = eval "use FS::cust_main::import_charges::$mod; ". + "\\%FS::cust_main::import_charges::$mod\::info;"; + if ( $@ ) { + die "error using FS::cust_main::import_charges::$mod (skipping): $@\n" if $@; + next; + } + unless ( keys %$info ) { + warn "no %info hash found in FS::cust_main::import_charges::$mod, skipping\n"; + next; + } + warn "got import charges format info from FS::cust_main::import_charges::$mod: $info\n" if $DEBUG; + if ( exists($info->{'disabled'}) && $info->{'disabled'} ) { + warn "skipping disabled import charges format FS::cust_main::import_charges::$mod" if $DEBUG; + next; + } + $import_charges_info{$mod} = $info; + } +} + +tie my %import_formats, 'Tie::IxHash', + map { $_ => $import_charges_info{$_}->{'name'} } + sort { $import_charges_info{$a}->{'weight'} <=> $import_charges_info{$b}->{'weight'} } + grep { exists($import_charges_info{$_}->{'fields'}) } + keys %import_charges_info; + +sub import_formats { + %import_formats; +} =head1 NAME @@ -65,20 +107,10 @@ sub batch_charge { my @fields; my %charges; - if ( $format eq 'simple' ) { - @fields = qw( custnum agent_custid amount pkg ); - } elsif ( $format eq 'ooma' ) { - #below is gcet file. - #@fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); - @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); - - ##should charges to charge be a config option? - %charges = ( - 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', - 'FEDERAL TRS FUND' => '1', - 'FEDERAL UNIVERSAL SERVICE FUND' => '1', - 'STATE SALES TAX' => '1', - ); + + if ( $import_charges_info{$format} ) { + @fields = @{$import_charges_info{$format}->{'fields'}}; + %charges = %{$import_charges_info{$format}->{'charges'}}; } else { die "unknown format $format"; } diff --git a/FS/FS/cust_main/import_charges/gcet.pm b/FS/FS/cust_main/import_charges/gcet.pm new file mode 100644 index 000000000..83f956545 --- /dev/null +++ b/FS/FS/cust_main/import_charges/gcet.pm @@ -0,0 +1,26 @@ +package FS::cust_main::import_charges::gcet; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# gcet fields. +my @fields = ( 'userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'userfield9', 'userfield10', 'amount', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'userfield17', 'userfield18', 'pkg', 'userfield20', 'custnum', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = { + 'DISABILITY ACCESS/ENHANCED 911 SERVICES SURCHARGE' => '1', + 'FEDERAL TRS FUND' => '1', + 'FEDERAL UNIVERSAL SERVICE FUND' => '1', + 'STATE SALES TAX' => '1', +}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Gcet', + 'weight' => '30', + 'disabled' => '1', +); + +1; \ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/ooma.pm b/FS/FS/cust_main/import_charges/ooma.pm new file mode 100644 index 000000000..a43def239 --- /dev/null +++ b/FS/FS/cust_main/import_charges/ooma.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::ooma; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# ooma fields +my @fields = ('userfield1', 'userfield2', 'userfield3', 'userfield4', 'userfield5', 'userfield6', 'userfield7', 'userfield8', 'amount', 'userfield10', 'userfield11', 'userfield12', 'userfield13', 'userfield14', 'userfield15', 'userfield16', 'pkg', 'userfield18', 'custnum', 'userfield20', 'userfield21', 'userfield22', 'userfield23', 'userfield24', 'userfield25', ); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Ooma', + 'weight' => '10', + 'disabled' => '', +); + +1; \ No newline at end of file diff --git a/FS/FS/cust_main/import_charges/simple.pm b/FS/FS/cust_main/import_charges/simple.pm new file mode 100644 index 000000000..e039328ba --- /dev/null +++ b/FS/FS/cust_main/import_charges/simple.pm @@ -0,0 +1,21 @@ +package FS::cust_main::import_charges::simple; + +use strict; +use base qw( FS::cust_main::Import_Charges ); +use vars qw ( %info ); + +# simple field format +my @fields = ('custnum', 'agent_custid', 'amount', 'pkg'); +# hash of charges (pkg) to charge. if empty charge them all. +# '911 services' => '1', +my $charges = {}; + +%info = ( + 'fields' => [@fields], + 'charges' => $charges, + 'name' => 'Simple', + 'weight' => '1', + 'disabled' => '', +); + +1; \ No newline at end of file diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi index 4eacce13a..215cc4c9d 100644 --- a/httemplate/misc/cust_main-import_charges.cgi +++ b/httemplate/misc/cust_main-import_charges.cgi @@ -28,9 +28,9 @@ Import a CSV file containing customer charges. @@ -94,6 +94,8 @@ Field information: die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Import'); + tie my %formats, 'Tie::IxHash', FS::cust_main::Import_Charges->import_formats; + my $custbatch = time2str('webimport-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time); -- cgit v1.2.1
Format