Neues Feature: Chartpicker
authorG. Richardson <information@kivitendo-premium.de>
Tue, 13 Jan 2015 21:47:27 +0000 (22:47 +0100)
committerG. Richardson <information@kivitendo-premium.de>
Wed, 14 Jan 2015 14:31:14 +0000 (15:31 +0100)
Praktisch komplett analog zum Partpicker, danke Sven für die
Pionierleistung!

Es gibt natürlich ein paar Unterschiede bei den Filteroptionen, z.B.
kann man im Popup nur bebuchte Konten anzeigen lassen, oder die
Ergebnisliste nach der Kontenart einschränken. Es wird auch immer nur
eine Konto pro Spalte angezeigt, auch im Block Modus.

Hat der Benutzer FiBu-Rechte wird auch der aktuelle Kontensaldo
angezeigt. Hierfür wurden ein paar neue Methoden für Chart hinzugefügt.

16 files changed:
SL/Controller/Chart.pm [new file with mode: 0644]
SL/DB/Chart.pm
SL/DB/Manager/Chart.pm
SL/Presenter.pm
SL/Presenter/Chart.pm [new file with mode: 0644]
SL/Template/Plugin/L.pm
css/kivitendo/main.css
css/lx-office-erp/main.css
doc/changelog
js/autocomplete_chart.js [new file with mode: 0644]
js/kivi.js
js/locale/de.js
locale/de/all
templates/webpages/chart/_chart_picker_result.html [new file with mode: 0644]
templates/webpages/chart/chart_picker_search.html [new file with mode: 0644]
templates/webpages/chart/test_page.html [new file with mode: 0644]

diff --git a/SL/Controller/Chart.pm b/SL/Controller/Chart.pm
new file mode 100644 (file)
index 0000000..68c75bf
--- /dev/null
@@ -0,0 +1,116 @@
+package SL::Controller::Chart;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use Clone qw(clone);
+use SL::DB::Chart;
+use SL::Controller::Helper::GetModels;
+use SL::DB::Helper::Paginated;
+use SL::Locale::String qw(t8);
+use SL::JSON;
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(charts models chart) ],
+);
+
+sub action_ajax_autocomplete {
+  my ($self, %params) = @_;
+
+  my $value = $::form->{column} || 'description';
+
+  # if someone types something, and hits enter, assume he entered the full name.
+  # if something matches, treat that as sole match
+  # unfortunately get_models can't do more than one per package atm, so we do it
+  # the oldfashioned way.
+  if ($::form->{prefer_exact}) {
+    my $exact_matches;
+    # we still need the type filter so that we can't choose an illegal chart
+    # via exact_match if we have preset a link type, e.g. AR_paid
+    if (1 == scalar @{ $exact_matches = SL::DB::Manager::Chart->get_all(
+      query => [
+        SL::DB::Manager::Chart->type_filter($::form->{filter}{type}),
+        or => [
+          description => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
+          accno       => { ilike => $::form->{filter}{'all:substr:multi::ilike'} },
+        ]
+      ],
+      limit => 2,
+    ) }) {
+      $self->charts($exact_matches);
+    }
+  }
+
+  my @hashes = map {
+   +{
+     value       => $_->displayable_name,
+     label       => $_->displayable_name,
+     id          => $_->id,
+     accno       => $_->accno,
+     description => $_->description,
+    }
+  } @{ $self->charts }; # neato: if exact match triggers we don't even need the init_parts
+
+  $self->render(\ SL::JSON::to_json(\@hashes), { layout => 0, type => 'json', process => 0 });
+}
+
+sub action_test_page {
+  $_[0]->render('chart/test_page');
+}
+
+sub action_chart_picker_search {
+  $_[0]->render('chart/chart_picker_search', { layout => 0 }, charts => $_[0]->charts);
+}
+
+sub action_chart_picker_result {
+  $_[0]->render('chart/_chart_picker_result', { layout => 0 });
+}
+
+sub action_show {
+  my ($self) = @_;
+
+  if ($::request->type eq 'json') {
+    my $chart_hash;
+    if (!$self->chart) {
+      # TODO error
+    } else {
+      require Rose::DB::Object::Helpers;
+        $chart_hash                     = $self->chart->as_tree;
+        $chart_hash->{displayable_name} = $self->chart->displayable_name;
+    }
+
+    $self->render(\ SL::JSON::to_json($chart_hash), { layout => 0, type => 'json', process => 0 });
+  }
+}
+
+sub init_charts {
+
+  # disable pagination when hiding chart details = paginate when showing chart details
+  if ($::form->{hide_chart_details}) {
+    $_[0]->models->disable_plugin('paginated');
+  }
+
+  $_[0]->models->get;
+}
+
+sub init_chart {
+  SL::DB::Chart->new(id => $::form->{id} || $::form->{chart}{id})->load;
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller => $self,
+    sorted => {
+      _default  => {
+        by  => 'accno',
+        dir => 1,
+      },
+      accno       => t8('Account number'),
+      description => t8('Description'),
+    },
+  );
+}
+
+1;
index 40ac530..ee7ad6a 100644 (file)
@@ -4,6 +4,10 @@ use strict;
 
 use SL::DB::MetaSetup::Chart;
 use SL::DB::Manager::Chart;
+use SL::DBUtils;
+use Rose::DB::Object::Helpers qw(as_tree);
+use SL::DB::Helper::AccountingPeriod qw(get_balance_starting_date);
+use SL::Locale::String qw(t8);
 
 __PACKAGE__->meta->add_relationships(taxkeys => { type         => 'one to many',
                                                   class        => 'SL::DB::TaxKey',
@@ -27,6 +31,96 @@ sub get_active_taxkey {
     sort_by => "startdate DESC")->[0];
 }
 
+sub get_active_taxrate {
+  my ($self, $date) = @_;
+  $date ||= DateTime->today_local;
+  require SL::DB::Tax;
+  my $tax = SL::DB::Manager::Tax->find_by( id => $self->get_active_taxkey->tax_id );
+  return $tax->rate;
+}
+
+
+sub get_balance {
+  my ($self, %params) = @_;
+
+  return undef unless $self->id;
+
+  # return empty string if user doesn't have rights
+  return "" unless ($main::auth->assert('general_ledger', 1));
+
+  my $query = qq|SELECT SUM(amount) AS sum FROM acc_trans WHERE chart_id = ? AND transdate >= ? and transdate <= ?|;
+
+  my $startdate = $self->get_balance_starting_date;
+  my $today     = DateTime->today_local;
+
+  my ($balance)  = selectfirst_array_query($::form, $self->db->dbh, $query, $self->id, $startdate, $today);
+
+  return $balance;
+};
+
+sub formatted_balance_dc {
+  my ($self, %params) = @_;
+
+  # return empty string if user doesn't have rights
+  return "" unless ($main::auth->assert('general_ledger', 1));
+
+  # return empty string if chart has never been booked
+  return "" unless $self->has_transaction;
+
+  # return abs of current balance with the abbreviation for debit or credit behind it
+  my $balance = $self->get_balance || 0;
+  my $dc_abbreviation = $balance > 0 ? t8("Credit (one letter abbreviation)") : t8("Debit (one letter abbreviation)");
+  my $amount = $::form->format_amount(\%::myconfig, abs($balance), 2);
+
+  return "$amount $dc_abbreviation";
+};
+
+sub number_of_transactions {
+  my ($self) = @_;
+
+  my ($acc_trans) = $self->db->dbh->selectrow_array('select count(acc_trans_id) from acc_trans where chart_id = ?', {}, $self->id);
+
+  return $acc_trans;
+};
+
+sub has_transaction {
+  my ($self) = @_;
+
+  my ($id) = $self->db->dbh->selectrow_array('select acc_trans_id from acc_trans where chart_id = ? limit 1', {}, $self->id) || 0;
+
+  $id ? return 1 : return 0;
+
+};
+
+sub displayable_name {
+  my ($self) = @_;
+
+  return join ' ', grep $_, $self->accno, $self->description;
+}
+
+sub displayable_category {
+  my ($self) = @_;
+
+  return t8("Account Category E") if $self->category eq "E";
+  return t8("Account Category I") if $self->category eq "I";
+  return t8("Account Category A") if $self->category eq "A";
+  return t8("Account Category L") if $self->category eq "L";
+  return t8("Account Category Q") if $self->category eq "Q";
+  return t8("Account Category C") if $self->category eq "C";
+  return '';
+}
+
+sub date_of_last_transaction {
+  my ($self) = @_;
+
+  die unless $self->id;
+
+  return '' unless $self->has_transaction;
+
+  my ($transdate) = $self->db->dbh->selectrow_array('select max(transdate) from acc_trans where chart_id = ?', {}, $self->id);
+  return DateTime->from_lxoffice($transdate);
+}
+
 1;
 
 __END__
@@ -48,6 +142,35 @@ SL::DB::Chart - Rose database model for the "chart" table
 Returns the active tax key object for a given date. C<$date> defaults
 to the current date if undefined.
 
+=item C<get_active_taxrate $date>
+
+Returns the tax rate of the active tax key as determined by
+C<get_active_taxkey>.
+
+=item C<get_balance>
+
+Returns the current balance of the chart (sum of amount in acc_trans, positive
+or negative). The transactions are filtered by transdate, the maximum date is
+the current day, the minimum date is determined by get_balance_starting_date.
+
+The balance should be same as that in the balance report for that chart, with
+the asofdate as the current day, and the accounting_method "accrual".
+
+=item C<formatted_balance_dc>
+
+Returns a formatted version of C<get_balance>, taking the absolute value and
+adding the translated abbreviation for debit or credit after the number.
+
+=item C<has_transaction>
+
+Returns 1 or 0, depending whether the chart has a transaction in the database
+or not.
+
+=item C<date_of_last_transaction>
+
+Returns the date of the last transaction of the chart in the database, which
+may lie in the future.
+
 =back
 
 =head1 BUGS
@@ -58,4 +181,6 @@ Nothing here yet.
 
 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
 
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+
 =cut
index f7728ae..4d11960 100644 (file)
@@ -6,13 +6,113 @@ use SL::DB::Helper::Manager;
 use base qw(SL::DB::Helper::Manager);
 
 use SL::DB::Helper::Sorted;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Filtered;
+use SL::MoreCommon qw(listify);
 use DateTime;
 use SL::DBUtils;
+use Data::Dumper;
 
 sub object_class { 'SL::DB::Chart' }
 
 __PACKAGE__->make_manager_methods;
 
+__PACKAGE__->add_filter_specs(
+  type => sub {
+    my ($key, $value) = @_;
+    return __PACKAGE__->type_filter($value);
+  },
+  category => sub {
+    my ($key, $value) = @_;
+    return __PACKAGE__->category_filter($value);
+  },
+  selected_category => sub {
+    my ($key, $value) = @_;
+    return __PACKAGE__->selected_category_filter($value);
+  },
+  all => sub {
+    my ($key, $value) = @_;
+    return or => [ map { $_ => $value } qw(accno description) ]
+  },
+  booked => sub {
+    my ($key, $value) = @_;
+    return __PACKAGE__->booked_filter($value);
+  },
+);
+
+sub booked_filter {
+  my ($class, $booked) = @_;
+
+  $booked //= 0;
+  my @filter;
+
+  if ( $booked ) {
+     push @filter, ( id => [ \"SELECT distinct chart_id FROM acc_trans" ] );
+  };
+
+  return @filter;
+}
+
+sub selected_category_filter {
+  my ($class, $selected_categories) = @_;
+
+  my @selected_categories = @$selected_categories;
+
+  # if no category is selected, there is no filter and thus all charts of all
+  # categories are displayed, which is what we want.
+
+  return (category => \@$selected_categories);
+}
+
+sub type_filter {
+  my ($class, $type) = @_;
+
+  # filter by link or several defined custom types
+  # special types:
+  # bank, guv, balance
+
+  return () unless $type;
+
+  if ('HASH' eq ref $type) {
+    # this is to make selection like type => { AR_paid => 1, AP_paid => 1 } work
+    $type = [ grep { $type->{$_} } keys %$type ];
+  }
+
+  my @types = grep { $_ } listify($type);
+  my @filter;
+
+  for my $type (@types) {
+    if ( $type eq 'bank' ) {
+     push @filter, ( id => [ \"SELECT chart_id FROM bank_accounts" ] );
+    } elsif ( $type eq 'guv' ) {
+     push @filter, ( category => [ 'I', 'E' ] );
+    } elsif ( $type eq 'balance' ) {
+     push @filter, ( category => [ 'A', 'Q', 'L' ] );
+    } else {
+      push @filter, $class->link_filter($type);
+    };
+  };
+
+  return @filter > 2 ? (or => \@filter) : @filter;
+}
+
+sub category_filter {
+  my ($class, $category) = @_;
+
+  return () unless $category;
+
+  # filter for chart_picker if a category filter was passed via params
+
+  if ( ref $category eq 'HASH' ) {
+    # this is to make a selection like category => { I => 1, E => 1 } work
+    $category = [ grep { $category->{$_} } keys %$category ];
+  }
+
+  my @categories = grep { $_ } listify($category);
+
+  return (category => \@categories);
+}
+
 sub link_filter {
   my ($class, $link) = @_;
 
@@ -43,6 +143,16 @@ sub cache_taxkeys {
   }
 }
 
+sub _sort_spec {
+  (
+    default  => [ 'accno', 1 ],
+    # columns  => {
+    #   SIMPLE => 'ALL',
+    # },
+    nulls    => {},
+  );
+}
+
 1;
 
 __END__
@@ -77,4 +187,6 @@ Nothing here yet.
 
 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
 
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+
 =cut
index ee0e495..a9c73f8 100644 (file)
@@ -7,6 +7,7 @@ use parent qw(Rose::Object);
 use Carp;
 use Template;
 
+use SL::Presenter::Chart;
 use SL::Presenter::CustomerVendor;
 use SL::Presenter::DeliveryOrder;
 use SL::Presenter::EscapedText;
diff --git a/SL/Presenter/Chart.pm b/SL/Presenter/Chart.pm
new file mode 100644 (file)
index 0000000..025fa80
--- /dev/null
@@ -0,0 +1,295 @@
+package SL::Presenter::Chart;
+
+use strict;
+
+use SL::DB::Chart;
+
+use Exporter qw(import);
+use Data::Dumper;
+our @EXPORT = qw(chart_picker chart);
+
+use Carp;
+
+sub chart {
+  my ($self, $chart, %params) = @_;
+
+  $params{display} ||= 'inline';
+
+  croak "Unknown display type '$params{display}'" unless $params{display} =~ m/^(?:inline|table-cell)$/;
+
+  my $text = join '', (
+    $params{no_link} ? '' : '<a href="am.pl?action=edit_account&id=' . $self->escape($chart->id) . '">',
+    $self->escape($chart->accno),
+    $params{no_link} ? '' : '</a>',
+  );
+  return $self->escaped_text($text);
+}
+
+sub chart_picker {
+  my ($self, $name, $value, %params) = @_;
+
+  $value = SL::DB::Manager::Chart->find_by(id => $value) if $value && !ref $value;
+  my $id = delete($params{id}) || $self->name_to_id($name);
+  my $fat_set_item = delete $params{fat_set_item};
+
+  my @classes = $params{class} ? ($params{class}) : ();
+  push @classes, 'chart_autocomplete';
+  push @classes, 'chartpicker_fat_set_item' if $fat_set_item;
+
+  my $ret =
+    $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) .
+    join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type category choose booked)) .
+    $self->input_tag("", (ref $value && $value->can('displayable_name')) ? $value->displayable_name : '', id => "${id}_name", %params);
+
+  $::request->layout->add_javascripts('autocomplete_chart.js');
+  $::request->presenter->need_reinit_widgets($id);
+
+  $self->html_tag('span', $ret, class => 'chart_picker');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Presenter::Chart - Chart related presenter stuff
+
+=head1 SYNOPSIS
+
+  # Create an html link for editing/opening a chart
+  my $object = SL::DB::Manager::Chart->get_first;
+  my $html   = SL::Presenter->get->chart($object, display => 'inline');
+
+see also L<SL::Presenter>
+
+=head1 DESCRIPTION
+
+see L<SL::Presenter>
+
+=head1 FUNCTIONS
+
+=over 2
+
+=item C<chart, $object, %params>
+
+Returns a rendered version (actually an instance of
+L<SL::Presenter::EscapedText>) of the chart object C<$object>
+
+C<%params> can include:
+
+=over 4
+
+=item * display
+
+Either C<inline> (the default) or C<table-cell>. At the moment both
+representations are identical and produce the chart's name linked
+to the corresponding 'edit' action.
+
+=back
+
+=back
+
+=over 2
+
+=item C<chart_picker $name, $value, %params>
+
+All-in-one picker widget for charts. The code was originally copied and adapted
+from the part picker. The name will be both id and name of the resulting hidden
+C<id> input field (but the ID can be overwritten with C<$params{id}>).
+
+An additional dummy input will be generated which is used to find
+chart. For a detailed description of its behaviour, see section
+C<CHART PICKER SPECIFICATION>.
+
+For some examples of usage see the test page at controller.pl?action=Chart/test_page
+
+C<$value> can be a chart id or a C<Rose::DB:Object> instance.
+
+C<%params> can include:
+
+=over 4
+
+=item * category
+
+If C<%params> contains C<category> only charts of this category will be
+available for selection (in the autocompletion and in the popup).
+
+You may comma separate multiple categories, e.g C<A,Q,L>.
+
+In SL::DB::Manager::Chart there is also a filter called C<selected_category>,
+which filters the possible charts according to the category checkboxes the user
+selects in the popup window. This filter may further restrict the results of
+the filter category, but the user is not able to "break out of" the limits
+defined by C<category>. In fact if the categories are restricted by C<category>
+the popup template should only show checkboxes for those categories.
+
+=item * type
+
+If C<%params> contains C<type> only charts of this type will be used for
+autocompletion, i.e. the selection is filtered. You may comma separate multiple
+types.
+
+Type is usually a filter for link: C<AR,AR_paid>
+
+Type can also be a specially defined type: C<guv>, C<balance>, C<bank>
+
+See the type filter in SL::DB::Manager::Chart.
+
+=item * choose
+
+If C<%params> is passed with choose=1 the input of the filter field in the
+popup window is cleared. This is useful if a chart was already selected and we
+want to choose a new chart and immediately see all options.
+
+=item * fat_set_item
+
+If C<%params> is passed with fat_set_item=1 the contents of the selected chart
+object (the elements of the database chart table) are passed back via JSON into
+the item object. There is an example on the test page.
+
+Without fat_set_item only the variables id and name (displayable name) are
+available.
+
+=back
+
+C<chart_picker> will register its javascript for inclusion in the next header
+rendering. If you write a standard controller that only calls C<render> once, it
+will just work.  In case the header is generated in a different render call
+(multiple blocks, ajax, old C<bin/mozilla> style controllers) you need to
+include C<js/autocomplete_part.js> yourself.
+
+=back
+
+=head1 POPUP LAYER
+
+For users that don't regularly do bookkeeping and haven't memorised all the
+account numbers and names there are some filter options inside the popup window
+to quickly narrow down the possible matches. You can filter by
+
+=over 4
+
+=item * chart accno or description, inside the input field
+
+=item * accounts that have already been booked
+
+=item * by category (AIELQC)
+
+By selecting category checkboxes the list of available charts can be
+restricted. If all checkboxes are unchecked all possible charts are shown.
+
+=back
+
+There are two views in the list of accounts. By default all possible accounts are shown as a long list.
+
+But you can also show more information, in this case the resulting list is automatically paginated:
+
+=over 4
+
+=item * the balance of the account (as determined by SL::DB::Chart get_balance, which checks for user rights)
+
+=item * the category
+
+=item * the invoice date of the last transaction (may lie in the future)
+
+=back
+
+The partpicker also has two views, but whereas the compact block view of the
+partpicker allows part boxes to be aligned in two columns, the chartpicker
+block view still only shows one chart per row, because there is more
+information and the account names can be quite long. This behaviour is
+determined by css, however, and could be changed (div.cpc_block).  The downside
+of this is that you have to scroll the window to reach the pagination controls.
+
+The default view of the display logic is to use block view, so we don't have to
+pass any parameters in the pagination GET. However, the default view for the
+user is the list view, so the popup window is opened with the "Hide additional
+information" select box already ticked.
+
+=head1 CHART PICKER SPECIFICATION
+
+The following list of design goals were applied:
+
+=over 4
+
+=item *
+
+Charts should not be perceived by the user as distinct inputs of chart number and
+description but as a single object
+
+=item *
+
+Easy to use without documentation for novice users
+
+=item *
+
+Fast to use with keyboard for experienced users
+
+=item *
+
+Possible to use without any keyboard interaction for mouse (or touchscreen)
+users
+
+=item *
+
+Must not leave the current page in event of ambiguity (cf. current select_item
+mechanism)
+
+=item *
+
+Should not require a feedback/check loop in the common case
+
+=item *
+
+Should not be constrained to exact matches
+
+=back
+
+The implementation consists of the following parts which will be referenced later:
+
+=over 4
+
+=item 1
+
+A hidden input (id input), used to hold the id of the selected part. The only
+input that gets submitted
+
+=item 2
+
+An input (dummy input) containing a description of the currently selected chart,
+also used by the user to search for charts
+
+=item 3
+
+A jquery.autocomplete mechanism attached to the dummy field
+
+=item 4
+
+A popup layer for both feedback and input of additional data in case of
+ambiguity.
+
+=item 5
+
+An internal status of the chart picker, indicating whether id input and dummy
+input are consistent. After leaving the dummy input the chart picker must
+place itself in a consistent status.
+
+=item 6
+
+A clickable icon (popup trigger) attached to the dummy input, which triggers the popup layer.
+
+=back
+
+=head1 BUGS
+
+None atm :)
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+
+=cut
index 2e2ae9f..720c5b7 100644 (file)
@@ -67,6 +67,7 @@ sub input_tag     { return _call_presenter('input_tag',     @_); }
 sub truncate      { return _call_presenter('truncate',      @_); }
 sub simple_format { return _call_presenter('simple_format', @_); }
 sub part_picker   { return _call_presenter('part_picker',   @_); }
+sub chart_picker  { return _call_presenter('chart_picker',  @_); }
 sub customer_vendor_picker   { return _call_presenter('customer_vendor_picker',   @_); }
 
 sub _set_id_attribute {
index ea229ba..b20f60d 100644 (file)
@@ -360,13 +360,18 @@ label {
 .part_picker {
   padding-right: 16px;
 }
+.chart_picker {
+  padding-right: 16px;
+}
 .customer-vendor-picker-undefined,
+.chartpicker-undefined,
 .partpicker-undefined {
   color: red;
   font-style: italic;
 }
 
-div.part_picker_part {
+div.part_picker_part,
+div.chart_picker_chart {
   padding: 5px;
   margin: 5px;
   border: 1px;
@@ -378,30 +383,56 @@ div.part_picker_part {
   background-color: white;
   cursor: pointer;
 }
-div.part_picker_part:hover {
+div.part_picker_part:hover,
+div.chart_picker_chart:hover {
   background-color: #CCCCCC;
   color: #FE5F14;
   border-color: gray;
 }
+
 div.ppp_block {
   overflow:hidden;
   float:left;
   width: 350px;
 }
-div.ppp_block span.ppp_block_number {
+/* div.cpc_block { */
+/*   overflow:hidden; */
+/*   float:left; */
+/*   width: 350px; */
+/* } */
+div.ppp_block span.ppp_block_number,
+div.cpc_block span.cpc_block_number
+{
   float:left;
 }
 div.ppp_block span.ppp_block_description {
   float:right;
+  margin-left:1em;
   font-weight:bold;
 }
-div.ppp_line span.ppp_block_description {
+div.cpc_block span.cpc_block_description {
+  float:left;
   margin-left:1em;
   font-weight:bold;
 }
-div.ppp_line span.ppp_block_sellprice {
+div.ppp_line span.ppp_block_description,
+div.cpc_line span.cpc_block_description
+{
+  margin-left:1em;
+  font-weight:bold;
+}
+div.cpc_block span.cpc_block_balance {
+  float:right;
+}
+div.cpc_block span.cpc_line_balance {
   display:none;
 }
+div.cpc_line span.cpc_block_second_row {
+  display:none;
+}
+div.cpc_block span.cpc_block_second_row {
+  font-size:80%;
+}
 span.toggle_selected {
   font-weight: bold;
 }
index 812879a..176d62d 100644 (file)
@@ -406,46 +406,74 @@ label {
 .part_picker {
   padding-right: 16px;
 }
+.chart_picker {
+  padding-right: 16px;
+}
 .customer-vendor-picker-undefined,
+.chartpicker-undefined,
 .partpicker-undefined {
   color: red;
   font-style: italic;
 }
-div.part_picker_part {
+
+div.part_picker_part,
+div.chart_picker_chart {
   padding: 5px;
   margin: 5px;
   border: 1px;
   border-color: darkgray;
   border-style: solid;
-  -webkit-border-radius: 2px;
-  -moz-border-radius: 2px;
-  border-radius: 2px;
-  background-color: whitesmoke;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  background-color: white;
   cursor: pointer;
 }
-div.part_picker_part:hover {
-  background-color: lightgray;
+div.part_picker_part:hover,
+div.chart_picker_chart:hover {
+  background-color: #CCCCCC;
+  color: #FE5F14;
   border-color: gray;
 }
+
 div.ppp_block {
   overflow:hidden;
   float:left;
-  width: 350px;
+  width: 350;
 }
-div.ppp_block span.ppp_block_number {
+div.ppp_block span.ppp_block_number,
+div.cpc_block span.cpc_block_number
+{
   float:left;
 }
 div.ppp_block span.ppp_block_description {
   float:right;
+  margin-left:1em;
+  font-weight:bold;
+}
+div.cpc_block span.cpc_block_description {
+  float:left;
+  margin-left:1em;
   font-weight:bold;
 }
-div.ppp_line span.ppp_block_description {
+div.ppp_line span.ppp_block_description,
+div.cpc_line span.cpc_block_description
+{
   margin-left:1em;
   font-weight:bold;
 }
-div.ppp_line span.ppp_block_sellprice {
+div.cpc_block span.cpc_block_balance {
+  float:right;
+}
+div.cpc_block span.cpc_line_balance {
   display:none;
 }
+div.cpc_line span.cpc_block_second_row {
+  display:none;
+}
+div.cpc_block span.cpc_block_second_row {
+  font-size:80%;
+}
 span.toggle_selected {
   font-weight: bold;
 }
index ba34485..2308512 100644 (file)
@@ -112,6 +112,10 @@ Kleinere neue Features und Detailverbesserungen:
   jetzt eindeutig anhand der Variablen zu unterscheiden.
   Ggf. müssen bestehende Druckvorlagen angepasst werden (s.a. http://blog.kivitendo-premium.de/?p=351).
 
+- Chartpicker
+  Analog zum Partpicker steht nun ein intelligenter Picker für Konten zur
+  Verfügung.
+
 2014-02-28 - Release 3.1.0
 
 Größere neue Features:
diff --git a/js/autocomplete_chart.js b/js/autocomplete_chart.js
new file mode 100644 (file)
index 0000000..d5763a4
--- /dev/null
@@ -0,0 +1,278 @@
+namespace('kivi', function(k){
+  k.ChartPicker = function($real, options) {
+    // short circuit in case someone double inits us
+    if ($real.data("chart_picker"))
+      return $real.data("chart_picker");
+
+    var KEY = {
+      ESCAPE: 27,
+      ENTER:  13,
+      TAB:    9,
+      LEFT:   37,
+      RIGHT:  39,
+      PAGE_UP: 33,
+      PAGE_DOWN: 34,
+    };
+    var CLASSES = {
+      PICKED:       'chartpicker-picked',
+      UNDEFINED:    'chartpicker-undefined',
+      FAT_SET_ITEM: 'chartpicker_fat_set_item',
+    }
+    var o = $.extend({
+      limit: 20,
+      delay: 50,
+      fat_set_item: $real.hasClass(CLASSES.FAT_SET_ITEM),
+    }, options);
+    var STATES = {
+      PICKED:    CLASSES.PICKED,
+      UNDEFINED: CLASSES.UNDEFINED
+    }
+    var real_id = $real.attr('id');  // id of selected chart_picker, e.g. bank_id
+    var $dummy  = $('#' + real_id + '_name');  // the input_field of the chart_picker
+    var $type   = $('#' + real_id + '_type');  // hidden input_field of the chart_picker, added in Presenter
+    var $category = $('#' + real_id + '_category');  // hidden input_field of the chart_picker, added in Presenter, never changes
+    var $choose = $('#' + real_id + '_choose');
+    var $booked = $('#' + real_id + '_booked');
+    var state   = STATES.PICKED;
+    var last_real = $real.val();
+    var last_dummy = $dummy.val();
+    var timer;
+
+    function open_dialog () {
+      // console.log('open_dialog');
+      // console.log($type);
+      // console.log(real_id);
+      k.popup_dialog({
+        url: 'controller.pl?action=Chart/chart_picker_search',
+        // data that can be accessed in template chart_picker_search via FORM.boss
+        data: $.extend({  // add id of part to the rest of the data in ajax_data, e.g. no_paginate, booked, ...
+          real_id: real_id,
+          hide_chart_details: 1,  // gets overwritten by ajax_data
+          select: 1,
+        }, ajax_data($dummy.val())),
+        id: 'chart_selection',
+        dialog: {
+          title: k.t8('Chart picker'),
+          width: 600,
+          height: 800,
+        }
+      });
+      window.clearTimeout(timer);
+      return true;
+    }
+
+    function ajax_data(term) {
+      var categories = $("#category input:checkbox:checked").map(function(){ return $(this).val(); }).get();
+      var data = {
+        'filter.all:substr:multi::ilike': term,
+        'filter.selected_category': categories,
+        'filter.booked': $('#booked').prop('checked') ? 1 : 0,
+        hide_chart_details: $('#hide_chart_details').prop('checked') ? 1 : 0,
+        booked:        $booked && $booked.val() ? $booked.val() : '',
+        choose:        $choose && $choose.val() ? $choose.val() : '',
+        current:       $real.val(),
+      };
+
+      if ($type && $type.val())
+        data['filter.type'] = $type.val().split(',');
+
+      if ($category && $category.val())
+        data['filter.category'] = $category.val().split(',');
+
+      return data;
+    }
+
+    function set_item (item) {
+      if (item.id) {
+        $real.val(item.id);
+        // autocomplete ui has name, ajax items have description
+        $dummy.val(item.name ? item.name : item.value);
+      } else {
+        $real.val('');
+        $dummy.val('');
+      }
+      state = STATES.PICKED;
+      last_real = $real.val();
+      last_dummy = $dummy.val();
+      last_unverified_dummy = $dummy.val();
+      $real.trigger('change');
+
+      if (o.fat_set_item && item.id) {
+        $.ajax({
+          url: 'controller.pl?action=Chart/show.json',
+          data: { id: item.id },
+          success: function(rsp) {
+            $real.trigger('set_item:ChartPicker', rsp);
+          },
+        });
+      } else {
+        $real.trigger('set_item:ChartPicker', item);
+      }
+      annotate_state();
+    }
+
+    function make_defined_state () {
+      if (state == STATES.PICKED) {
+        annotate_state();
+        return true
+      } else if (state == STATES.UNDEFINED && $dummy.val() == '')
+        set_item({})
+      else {
+        last_unverified_dummy = $dummy.val();
+        set_item({ id: last_real, name: last_dummy })
+      }
+      annotate_state();
+    }
+
+    function annotate_state () {
+      if (state == STATES.PICKED)
+        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
+      else if (state == STATES.UNDEFINED && $dummy.val() == '')
+        $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED);
+      else {
+        last_unverified_dummy = $dummy.val();
+        $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED);
+      }
+    }
+
+    function update_results () {
+      $.ajax({
+        url: 'controller.pl?action=Chart/chart_picker_result',
+        data: $.extend({
+            'real_id': $real.val(),
+        }, ajax_data(function(){ var val = $('#chart_picker_filter').val(); return val === undefined ? '' : val })),
+        success: function(data){ $('#chart_picker_result').html(data) }
+      });
+    };
+
+    function result_timer (event) {
+      if (!$('hide_chart_details').prop('checked')) {
+        if (event.keyCode == KEY.PAGE_UP) {
+          $('#chart_picker_result a.paginate-prev').click();
+          return;
+        }
+        if (event.keyCode == KEY.PAGE_DOWN) {
+          $('#chart_picker_result a.paginate-next').click();
+          return;
+        }
+      }
+      window.clearTimeout(timer);
+      timer = window.setTimeout(update_results, 100);
+    }
+
+    function close_popup() {
+      $('#chart_selection').dialog('close');
+    };
+
+    $dummy.autocomplete({
+      source: function(req, rsp) {
+        $.ajax($.extend(o, {
+          url:      'controller.pl?action=Chart/ajax_autocomplete',
+          dataType: "json",
+          // autoFocus: true,
+          data:     ajax_data(req.term),
+          success:  function (data){ rsp(data) }
+        }));
+      },
+      select: function(event, ui) {
+        set_item(ui.item);
+      },
+    });
+    /*  In case users are impatient and want to skip ahead:
+     *  Capture <enter> key events and check if it's a unique hit.
+     *  If it is, go ahead and assume it was selected. If it wasn't don't do
+     *  anything so that autocompletion kicks in.  For <tab> don't prevent
+     *  propagation. It would be nice to catch it, but javascript is too stupid
+     *  to fire a tab event later on, so we'd have to reimplement the "find
+     *  next active element in tabindex order and focus it".
+     */
+    /* note:
+     *  event.which does not contain tab events in keypressed in firefox but will report 0
+     *  chrome does not fire keypressed at all on tab or escape
+     */
+    $dummy.keydown(function(event){
+      if (event.which == KEY.ENTER || event.which == KEY.TAB) {
+        // if string is empty assume they want to delete
+        if ($dummy.val() == '') {
+          set_item({});
+          return true;
+        } else if (state == STATES.PICKED) {
+          return true;
+        }
+        if (event.which == KEY.TAB) event.preventDefault();
+        $.ajax({
+          url: 'controller.pl?action=Chart/ajax_autocomplete',
+          dataType: "json",
+          data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ),
+          success: function (data) {
+            if (data.length == 1) {
+              set_item(data[0]);
+              if (event.which == KEY.ENTER)
+                $('#update_button').click();
+            } else if (data.length > 1) {
+             if (event.which == KEY.ENTER)
+                open_dialog();
+            } else {
+            }
+            annotate_state();
+          }
+        });
+        if (event.which == KEY.ENTER)
+          return false;
+      } else {
+        state = STATES.UNDEFINED;
+      }
+    });
+
+    $dummy.blur(function(){
+      window.clearTimeout(timer);
+      timer = window.setTimeout(annotate_state, 100);
+    });
+
+    // now add a picker div after the original input
+    var pcont  = $('<span>').addClass('position-absolute');
+    var picker = $('<div>');
+    $dummy.after(pcont);
+    pcont.append(picker);
+    picker.addClass('icon16 crm--search').click(open_dialog);
+
+    var cp = {
+      real:           function() { return $real },
+      dummy:          function() { return $dummy },
+      type:           function() { return $type },
+      category:       function() { return $category },
+      update_results: update_results,
+      result_timer:   result_timer,
+      set_item:       set_item,
+      reset:          make_defined_state,
+      is_defined_state: function() { return state == STATES.PICKED },
+      init_results:    function () {
+        $('div.chart_picker_chart').each(function(){
+          $(this).click(function(){
+            set_item({
+              id:   $(this).children('input.chart_picker_id').val(),
+              name: $(this).children('input.chart_picker_description').val(),  // hidden field
+            });
+            close_popup();
+            $dummy.focus();
+            return true;
+          });
+        });
+        $('#chart_selection').keydown(function(e){
+           if (e.which == KEY.ESCAPE) {
+             close_popup();
+             $dummy.focus();
+           }
+        });
+      }
+    }
+    $real.data('chart_picker', cp);
+    return cp;
+  }
+});
+
+$(function(){
+  $('input.chart_autocomplete').each(function(i,real){
+    kivi.ChartPicker($(real));
+  })
+});
index 5ae742c..6ef7df7 100644 (file)
@@ -132,6 +132,12 @@ namespace("kivi", function(ns) {
         kivi.CustomerVendorPicker($(elt));
       });
 
+    if (ns.ChartPicker)
+      ns.run_once_for('input.chart_autocomplete', 'chart_picker', function(elt) {
+        kivi.ChartPicker($(elt));
+      });
+
+
     var func = kivi.get_function_by_name('local_reinit_widgets');
     if (func)
       func();
index a525b21..4b5d5e3 100644 (file)
@@ -11,6 +11,7 @@ namespace("kivi").setupLocale({
 "Are you sure?":"Sind Sie sicher?",
 "Basic settings actions":"Aktionen zu Grundeinstellungen",
 "Cancel":"Abbrechen",
+"Chart picker":"Kontenauswahl",
 "Copy":"Kopieren",
 "Copy requirement spec":"Pflichtenheft kopieren",
 "Copy template":"Vorlage kopieren",
index 19890a9..8951368 100755 (executable)
@@ -486,6 +486,7 @@ $self->{texts} = {
   'Chart Type'                  => 'Kontentyp',
   'Chart balance'               => 'Kontensaldo',
   'Chart of Accounts'           => 'Kontenübersicht',
+  'Chart picker'                => 'Kontenauswahl',
   'Chartaccounts connected to this Tax:' => 'Konten, die mit dieser Steuer verknüpft sind:',
   'Check'                       => 'Scheck',
   'Check Details'               => 'Bitte Angaben überprüfen',
@@ -1224,6 +1225,7 @@ $self->{texts} = {
   'Here\'s an example command line:' => 'Hier ist eine Kommandozeile, die als Beispiel dient:',
   'Hide Filter'                 => 'Filter verbergen',
   'Hide by default'             => 'Standardm&auml;&szlig;ig verstecken',
+  'Hide chart details'          => 'Konteninformation verstecken',
   'Hide help text'              => 'Hilfetext verbergen',
   'Hide settings'               => 'Einstellungen verbergen',
   'Hints'                       => 'Hinweise',
@@ -1413,6 +1415,7 @@ $self->{texts} = {
   'Last row, description'       => 'Letzte Zeile, Artikelbeschreibung',
   'Last row, partnumber'        => 'Letzte Zeile, Nummer',
   'Last run at'                 => 'Letzte Ausführung um',
+  'Last transaction'            => 'Letzte Buchung',
   'Lastcost'                    => 'Einkaufspreis',
   'Lastcost (with X being a number)' => 'Einkaufspreis (X ist eine fortlaufende Zahl)',
   'Lead'                        => 'Kundenquelle',
@@ -1676,6 +1679,7 @@ $self->{texts} = {
   'One or more Perl modules missing' => 'Ein oder mehr Perl-Module fehlen',
   'Onhand only sets the quantity in master data, not in inventory. This is only a legacy info field and will be overwritten as soon as a inventory transfer happens.' => 'Das Import-Feld Auf Lager setzt nur die Menge in den Stammdaten, nicht im Lagerbereich. Dies ist historisch gewachsen nur ein Informationsfeld was mit dem tatsächlichen Wert überschrieben wird, sobald eine wirkliche Lagerbewegung stattfindet (DB-Trigger).',
   'Only Warnings and Errors'    => 'Nur Warnungen und Fehler',
+  'Only booked accounts'        => 'Nur bebuchte Konten',
   'Only due follow-ups'         => 'Nur f&auml;llige Wiedervorlagen',
   'Only groups that have been configured for the client the user logs in to will be considered.' => 'Allerdings werden nur diejenigen Gruppen herangezogen, die für den Mandanten konfiguriert sind.',
   'Only list customer\'s projects in sales records' => 'Nur Projekte des Kunden in Verkaufsbelegen anzeigen',
diff --git a/templates/webpages/chart/_chart_picker_result.html b/templates/webpages/chart/_chart_picker_result.html
new file mode 100644 (file)
index 0000000..806aca4
--- /dev/null
@@ -0,0 +1,25 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+[% FOREACH chart = SELF.charts %]
+<div class='chart_picker_chart [% FORM.hide_chart_details ? 'cpc_line' : 'cpc_block' %]'>
+  <input type='hidden' class='chart_picker_id' value='[% chart.id %]'>
+  <input type='hidden' class='chart_picker_description' value='[% chart.displayable_name %]'>
+  <span class='cpc_block_number'>[% chart.accno | html %]</span>
+  <span class='cpc_block_description'>[% chart.description | html %]</span>
+  [% UNLESS FORM.hide_chart_details %]
+    <span class='cpc_block_balance'>[% chart.formatted_balance_dc | html %]</span>
+    <div style='clear:both;'></div>
+    <span class='cpc_block_second_row'>[% LxERP.t8("Chart Type") %]: [% chart.displayable_category %] &nbsp;&nbsp;&nbsp; [% IF chart.has_transaction %][% LxERP.t8("Last transaction")%]: [% chart.date_of_last_transaction.to_kivitendo %][% END %]</span>
+  [% END %]
+</div>
+[%- END %]
+
+<div style='clear:both'></div>
+
+[% L.paginate_controls(target='#chart_picker_result', selector='#chart_picker_result', models=SELF.models) %]
+<script type='text/javascript'>
+  kivi.ChartPicker($('#'+$('#chart_picker_real_id').val())).init_results()
+</script>
diff --git a/templates/webpages/chart/chart_picker_search.html b/templates/webpages/chart/chart_picker_search.html
new file mode 100644 (file)
index 0000000..b4a697e
--- /dev/null
@@ -0,0 +1,57 @@
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE T8 %]
+
+<div style='overflow:hidden'>
+
+[% LxERP.t8("Filter") %]: [% L.input_tag('chart_picker_filter', SELF.filter.all_substr_multi__ilike, class='chart_picker_filter',  style="width: 400px") %]
+[% L.hidden_tag('chart_picker_real_id', FORM.real_id) %] <a href="javascript:void(0);" onclick="$('#chart_picker_filter').val('').focus();cp.update_results();">x</a>
+
+<div style="padding: 5px 0em 5px 0em">
+  <span>
+    [% L.checkbox_tag('booked', checked=FORM.booked, for_submit=1, label=LxERP.t8('Only booked accounts')) %]
+  </span>
+  <span class='float-right'>
+    [% L.checkbox_tag('hide_chart_details', checked=1, id='hide_chart_details', for_submit=1, label=LxERP.t8('Hide chart details')) %]
+  </span>
+</div>
+
+<div id="category">
+  [% LxERP.t8("Account Type") %]:
+
+  [% IF FORM.filter.category %]
+    [% FOREACH var IN FORM.filter.category %]
+      [% IF var == 'A' %][% L.checkbox_tag('SELF.filter.selected_category_A', checked=1, value ='A', label=LxERP.t8('Assets')) %]   [% END %]
+      [% IF var == 'L' %][% L.checkbox_tag('SELF.filter.selected_category_L', checked=1, value ='L', label=LxERP.t8('Liability')) %][% END %]
+      [% IF var == 'Q' %][% L.checkbox_tag('SELF.filter.selected_category_Q', checked=1, value ='Q', label=LxERP.t8('Equity')) %]   [% END %]
+      [% IF var == 'I' %][% L.checkbox_tag('SELF.filter.selected_category_I', checked=1, value ='I', label=LxERP.t8('Revenue')) %]  [% END %]
+      [% IF var == 'E' %][% L.checkbox_tag('SELF.filter.selected_category_E', checked=1, value ='E', label=LxERP.t8('Expense')) %]  [% END %]
+    [% END %]
+  [% ELSE %]
+    [% L.checkbox_tag('SELF.filter.selected_category_A', checked=0, value ='A', label=LxERP.t8('Assets')) %]
+    [% L.checkbox_tag('SELF.filter.selected_category_L', checked=0, value ='L', label=LxERP.t8('Liability')) %]
+    [% L.checkbox_tag('SELF.filter.selected_category_Q', checked=0, value ='Q', label=LxERP.t8('Equity')) %]
+    [% L.checkbox_tag('SELF.filter.selected_category_I', checked=0, value ='I', label=LxERP.t8('Revenue')) %]
+    [% L.checkbox_tag('SELF.filter.selected_category_E', checked=0, value ='E', label=LxERP.t8('Expense')) %]
+  [% END %]
+</div>
+
+<div style='clear:both'></div>
+<div id='chart_picker_result'></div>
+</div>
+
+<script type='text/javascript'>
+  var cp = kivi.ChartPicker($('#[% FORM.real_id %]'));
+  $(function(){
+    $('#chart_picker_filter').focus();
+    // empty input field upon opening if we just want to pick a field
+    [% IF FORM.choose %] $('#chart_picker_filter').val(''); [% END %]
+
+    cp.update_results();  // function from js/autocomplete_chart_js
+  });
+  $('#chart_picker_filter').keyup(cp.result_timer);
+  // use keyup instead of keypress to get backspace to work
+  // this messes up numblock though!
+  $("input[type='checkbox']").change(cp.update_results);
+</script>
diff --git a/templates/webpages/chart/test_page.html b/templates/webpages/chart/test_page.html
new file mode 100644 (file)
index 0000000..3e68e03
--- /dev/null
@@ -0,0 +1,58 @@
+[% USE L %]
+
+<h1>Chart Picker Testpage</h1>
+
+<div>
+
+<p>
+All charts: [% L.chart_picker('chart_id', '', style="width: 400px") %]text after icon<br>
+Only booked charts: [% L.chart_picker('chart_id_booked', '', booked=1, style="width: 400px") %]<br>
+All charts choose: [% L.chart_picker('chart_id_choose', '', choose=1, style="width: 400px") %]<br>
+</p>
+
+<p>
+Filter by link:<br>
+AR_paid: [% L.chart_picker('chart_id_ar_paid', undef, type='AR_paid', category='I,A' style="width: 400px") %]<br>
+AR: [% L.chart_picker('chart_id_ar', undef, type='AR', style="width: 400px") %]<br>
+AP: [% L.chart_picker('chart_id_ap', undef, type='AP', style="width: 400px") %]<br>
+AR or AP: [% L.chart_picker('chart_id_arap', undef, type='AR,AP', style="width: 400px") %]<br>
+IC_income,IC_sale: [% L.chart_picker('chart_id_icis', undef, type='IC_income,IC_sale', style="width: 400px") %]<br>
+IC_expense,IC_cogs: [% L.chart_picker('chart_id_icco', undef, type='IC_expense,IC_cogs', style="width: 400px") %]<br>
+</p>
+
+<p>
+Filter by category:<br>
+I: [% L.chart_picker('chart_id_i', undef, category='I', style="width: 400px") %]<br>
+IE: [% L.chart_picker('chart_id_ie', undef, category='I,E', style="width: 400px") %]<br>
+AQL: [% L.chart_picker('chart_id_aql', undef, category='A,Q,L', style="width: 400px") %]<br>
+</p>
+
+<p>
+Filter by special type:<br>
+GuV: [% L.chart_picker('chart_id_guv', undef, type='guv', style="width: 400px") %]<br>
+</p>
+
+<p>bank (fat): [% L.chart_picker('bank_id', '', type='bank', fat_set_item=1, choose=1, style="width: 400px") %]
+</p>
+<p id="banktext"></p>
+
+
+<p>
+[% FOREACH i IN [ 1 2 3 4 5 6 ] %]
+S [% i %]: [% L.chart_picker('credit_' _ i) %] - &nbsp;&nbsp;  H [% i %]: [% L.chart_picker('debit' _ i) %] <br>
+[% END %]
+</p>
+
+</div>
+
+<script type='text/javascript'>
+  $(function(){
+      // do something when a chartpicker with fat_set_item and a certain id changes
+      $('#bank_id').on('set_item:ChartPicker', function (e, item) {
+        console.log(item)
+        $('#banktext').html('Selected fat item with category ' + item.category);
+      })
+  });
+
+</script>
+