commit a9ffbf8c212a08a155f0645dba087a50bcd72684
Author: Geoffrey Richardson <information@kivitendo-premium.de>
Date:   Mon Sep 21 01:08:08 2015 +0200

    Shopmodul: Shop und ShopParts
    
    Shop - Einführung von ShopParts
    
    Erster Test mit einem Shop Reiter in Artikelmaske
    
    Conflicts:
            SL/IC.pm
    
    Conflicts:
            SL/DB/MetaSetup/ShopPart.pm
            templates/webpages/ic/tabs/_shop.html
    
    Shop - Rose shop_parts Verknüpfungen von Shop und Part
    
    Conflicts:
            SL/DB/Shop.pm
    
    Shop - SL/Shop and Shop Controller for editing shop configs
    
    Conflicts:
            SL/DB/Shop.pm
    
    Conflicts:
            SL/DB/Helper/ALL.pm
            SL/DB/Helper/Mappings.pm

diff --git a/SL/Controller/Shop.pm b/SL/Controller/Shop.pm
new file mode 100644
index 000000000..a79a361c2
--- /dev/null
+++ b/SL/Controller/Shop.pm
@@ -0,0 +1,146 @@
+package SL::Controller::Shop;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::DB::Default;
+use SL::DB::Manager::Shop;
+use SL::DB::Pricegroup;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(connectors price_types price_sources) ],
+  'scalar --get_set_init' => [ qw(shop) ]
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('load_types',    only => [ qw(new edit) ]);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->render('shops/list',
+                title => t8('Shops'),
+                SHOPS => SL::DB::Manager::Shop->get_all_sorted,
+               );
+}
+
+sub action_new {
+  my ($self) = @_;
+
+  $self->shop(SL::DB::Shop->new);
+  $self->render('shops/form', title       => t8('Add shop'));
+};
+
+sub action_edit {
+  my ($self) = @_;
+
+  $self->render('shops/form', title       => t8('Edit shop'));
+}
+
+sub action_create {
+  my ($self) = @_;
+
+  $self->shop(SL::DB::Shop->new);
+  $self->create_or_update;
+}
+
+sub action_update {
+  my ($self) = @_;
+  $self->create_or_update;
+}
+
+sub action_delete {
+  my ($self) = @_;
+
+  if ( eval { $self->shop->delete; 1; } ) {
+    flash_later('info',  $::locale->text('The shop has been deleted.'));
+  } else {
+    flash_later('error', $::locale->text('The shop has been used and cannot be deleted.'));
+  };
+  $self->redirect_to(action => 'list');
+}
+
+sub action_reorder {
+  my ($self) = @_;
+
+  SL::DB::Shop->reorder_list(@{ $::form->{shop_id} || [] });
+  $self->render(\'', { type => 'json' });
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('config');
+}
+
+sub init_shop {
+  SL::DB::Shop->new(id => $::form->{id})->load;
+}
+
+#
+# helpers
+#
+
+sub create_or_update {
+  my ($self) = @_;
+  my $is_new = !$self->shop->id;
+
+  my $params = delete($::form->{shop}) || { };
+
+  $self->shop->assign_attributes(%{ $params });
+
+  my @errors = $self->shop->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->render('shops/form',
+                   title => $is_new ? t8('Add shop') : t8('Edit shop'));
+    return;
+  }
+
+  $self->shop->save;
+
+  flash_later('info', $is_new ? t8('The shop has been created.') : t8('The shop has been saved.'));
+  $self->redirect_to(action => 'list');
+}
+
+sub load_types {
+  my ($self) = @_;
+  # data for the dropdowns when editing Shop configs
+
+  # hardcoded the possible connectors, which correspond to
+  # SL/ShopConnector/xxxx classes
+  $self->connectors( [ { id => "xtcommerce", description => "XT Commerce"},
+                       { id => "shopware",   description => "Shopware" },
+                       { id => "ideal",      description => "IDeal" }
+                     ]);
+
+  # whether the shop presents its prices as brutto or netto
+  $self->price_types( [ { id => "brutto", name => t8('brutto')}, { id => "netto", name => t8('netto') } ] );
+
+  # the possible price sources to use for the shops: sellprice, lastcost,
+  # listprice, or one of the pricegroups
+  my $pricesources;
+  push( @{ $pricesources } , { id => "sellprice", name => t8("Sellprice") },
+                             { id => "listprice", name => t8("Listprice") },
+                             { id => "lastcost",  name => t8("Lastcost") }
+                             );
+  my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
+  foreach my $pg ( @$pricegroups ) {
+    push( @{ $pricesources } , { id => $pg->id, name => $pg->pricegroup} );
+  };
+
+  $self->price_sources( $pricesources );
+};
+
+
+1;
diff --git a/SL/DB/Helper/ALL.pm b/SL/DB/Helper/ALL.pm
index f1059f44c..f38586b36 100644
--- a/SL/DB/Helper/ALL.pm
+++ b/SL/DB/Helper/ALL.pm
@@ -115,6 +115,8 @@ use SL::DB::SepaExport;
 use SL::DB::SepaExportItem;
 use SL::DB::SepaExportMessageId;
 use SL::DB::Shipto;
+use SL::DB::Shop;
+use SL::DB::ShopPart;
 use SL::DB::Status;
 use SL::DB::Tax;
 use SL::DB::TaxKey;
diff --git a/SL/DB/Helper/Mappings.pm b/SL/DB/Helper/Mappings.pm
index 5718d3eb9..357320982 100644
--- a/SL/DB/Helper/Mappings.pm
+++ b/SL/DB/Helper/Mappings.pm
@@ -195,6 +195,8 @@ my %kivitendo_package_names = (
   sepa_export_message_ids        => 'SepaExportMessageId',
   schema_info                    => 'schema_info',
   shipto                         => 'shipto',
+  shops                          => 'shop',
+  shop_parts                     => 'shop_part',
   status                         => 'status',
   tax                            => 'tax',
   taxkeys                        => 'tax_key',
@@ -208,6 +210,7 @@ my %kivitendo_package_names = (
   units_language                 => 'units_language',
   user_preferences               => 'user_preference',
   vendor                         => 'vendor',
+  web_shops                      => 'web_shop',
   warehouse                      => 'warehouse',
 );
 
diff --git a/SL/DB/Manager/Shop.pm b/SL/DB/Manager/Shop.pm
new file mode 100644
index 000000000..b7a19581e
--- /dev/null
+++ b/SL/DB/Manager/Shop.pm
@@ -0,0 +1,28 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::Shop;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::Shop' }
+
+use SL::DB::Helper::Sorted;
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return ( default => [ 'sortkey', 1 ],
+           columns => { SIMPLE => 'ALL' } );
+}
+
+sub get_default {
+    return $_[0]->get_first(where => [ obsolete => 0 ], sort_by => 'sortkey');
+}
+
+1;
+
+1;
diff --git a/SL/DB/MetaSetup/Shop.pm b/SL/DB/MetaSetup/Shop.pm
new file mode 100644
index 000000000..cc2d2705a
--- /dev/null
+++ b/SL/DB/MetaSetup/Shop.pm
@@ -0,0 +1,28 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::Shop;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('shops');
+
+__PACKAGE__->meta->columns(
+  connector    => { type => 'text' },
+  description  => { type => 'text' },
+  id           => { type => 'serial', not_null => 1 },
+  login        => { type => 'text' },
+  obsolete     => { type => 'boolean', default => 'false', not_null => 1 },
+  password     => { type => 'text' },
+  port         => { type => 'integer' },
+  price_source => { type => 'text' },
+  pricetype    => { type => 'text' },
+  sortkey      => { type => 'integer' },
+  url          => { type => 'text' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
diff --git a/SL/DB/MetaSetup/ShopPart.pm b/SL/DB/MetaSetup/ShopPart.pm
new file mode 100644
index 000000000..0133c0388
--- /dev/null
+++ b/SL/DB/MetaSetup/ShopPart.pm
@@ -0,0 +1,53 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ShopPart;
+
+use strict;
+
+<<<<<<< HEAD
+use parent qw(SL::DB::Object);
+=======
+use base qw(SL::DB::Object);
+>>>>>>> Shop - Einführung von ShopParts
+
+__PACKAGE__->meta->table('shop_parts');
+
+__PACKAGE__->meta->columns(
+  active              => { type => 'boolean', default => 'false', not_null => 1 },
+  active_price_source => { type => 'text' },
+  front_page          => { type => 'boolean', default => 'false', not_null => 1 },
+  id                  => { type => 'serial', not_null => 1 },
+  itime               => { type => 'timestamp', default => 'now()' },
+  last_update         => { type => 'timestamp' },
+  metatag_description => { type => 'text' },
+  metatag_keywords    => { type => 'text' },
+  metatag_title       => { type => 'text' },
+  mtime               => { type => 'timestamp' },
+  part_id             => { type => 'integer', not_null => 1 },
+  shop_category       => { type => 'array' },
+  shop_description    => { type => 'text' },
+  shop_id             => { type => 'integer', not_null => 1 },
+  show_date           => { type => 'date' },
+  sortorder           => { type => 'integer' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->unique_keys([ 'shop_id', 'part_id' ]);
+
+__PACKAGE__->meta->allow_inline_column_values(1);
+
+__PACKAGE__->meta->foreign_keys(
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+  },
+
+  shop => {
+    class       => 'SL::DB::Shop',
+    key_columns => { shop_id => 'id' },
+  },
+);
+
+1;
+;
diff --git a/SL/DB/Part.pm b/SL/DB/Part.pm
index c2cb641ae..21dedb502 100644
--- a/SL/DB/Part.pm
+++ b/SL/DB/Part.pm
@@ -29,6 +29,7 @@ __PACKAGE__->meta->add_relationships(
     type         => 'one to many',
     class        => 'SL::DB::Price',
     column_map   => { id => 'parts_id' },
+    manager_args => { with_objects => [ 'pricegroup' ] }
   },
   makemodels     => {
     type         => 'one to many',
@@ -53,6 +54,11 @@ __PACKAGE__->meta->add_relationships(
     query_args      => [ what_done => 'part' ],
     manager_args    => { sort_by => 'itime' },
   },
+  shop_parts     => {
+    type         => 'one to many',
+    class        => 'SL::DB::ShopPart',
+    column_map   => { id => 'part_id' },
+  },
 );
 
 __PACKAGE__->meta->initialize;
diff --git a/SL/DB/Shop.pm b/SL/DB/Shop.pm
new file mode 100644
index 000000000..073c4add6
--- /dev/null
+++ b/SL/DB/Shop.pm
@@ -0,0 +1,31 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Shop;
+
+use strict;
+
+use SL::DB::MetaSetup::Shop;
+use SL::DB::Manager::Shop;
+use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->add_relationships(
+  shop_parts     => {
+    type         => 'one to many',
+    class        => 'SL::DB::ShopPart',
+    column_map   => { id => 'shop_id' },
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+
+  push @errors, $::locale->text('The description is missing.') unless $self->{description};
+
+  return @errors;
+}
+1;
diff --git a/SL/Shop.pm b/SL/Shop.pm
new file mode 100644
index 000000000..2021d96db
--- /dev/null
+++ b/SL/Shop.pm
@@ -0,0 +1,52 @@
+package SL::Shop;
+
+use strict;
+
+use parent qw(Rose::Object);
+
+# __PACKAGE__->run_before('check_auth');
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar'                => [ qw(config) ],
+  'scalar --get_set_init' => [ qw(connector) ],
+);
+
+sub init_connector {
+  my ($self) = @_;
+  # determine the connector from the connector type in the webshop config
+  return SL::ShopConnector::ALL->shop_connector_class_by_name($self->config->connector)->new( config => $self->config); 
+
+};
+
+1;
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+SL::WebShop - Do stuff with WebShop instances
+
+=head1 SYNOPSIS
+
+  my $config = SL::DB::Manager::WebShop->get_first();
+  my $shop = SL::WebShop->new( config => $config );
+
+From the config we know which Connector class to load, save in $shop->connector
+and do stuff from there:
+
+  $shop->connector->get_new_orders;
+
+=head1 FUNCTIONS
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson <lt>information@kivitendo-premium.deE<gt>
+
+=cut
+
diff --git a/menus/user/00-erp.yaml b/menus/user/00-erp.yaml
index d7653c276..7ba532e1f 100644
--- a/menus/user/00-erp.yaml
+++ b/menus/user/00-erp.yaml
@@ -1242,6 +1242,12 @@
   module: am.pl
   params:
     action: list_warehouses
+- parent: system
+  id: system_shops
+  name: Web shops
+  order: 2350
+  params:
+    action: Shop/list
 - parent: system
   id: system_import_csv
   name: Import CSV
diff --git a/sql/Pg-upgrade2/shops.sql b/sql/Pg-upgrade2/shops.sql
new file mode 100644
index 000000000..a1dbfc0e9
--- /dev/null
+++ b/sql/Pg-upgrade2/shops.sql
@@ -0,0 +1,18 @@
+-- @tag: shops
+-- @description: Tabelle für Shops
+-- @depends: release_3_3_0
+-- @ignore: 0
+
+CREATE TABLE shops (
+  id SERIAL PRIMARY KEY,
+  description text,
+  obsolete BOOLEAN NOT NULL DEFAULT false,
+  sortkey INTEGER,
+  connector text,     -- hardcoded options, e.g. xtcommerce, shopware
+  pricetype text,     -- netto/brutto
+  price_source text,  -- sellprice/listprice/lastcost or pricegroup id
+  url text,
+  port INTEGER,
+  login text,  -- "user" is reserved
+  password text
+);
diff --git a/templates/webpages/ic/tabs/_shop.html b/templates/webpages/ic/tabs/_shop.html
new file mode 100644
index 000000000..fcc09e7c0
--- /dev/null
+++ b/templates/webpages/ic/tabs/_shop.html
@@ -0,0 +1,85 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
+<<<<<<< HEAD
+[%- USE Dumper %]
+[%- USE JavaScript -%]
+<div id="shop_variables">
+ <h2>[% LxERP.t8("Active shops:") %]</h2>
+ <table width="100%">
+  <thead>
+  <tr class="listheading">
+   <th>[% LxERP.t8("Shop") %]</th>
+   <th>[% LxERP.t8("Active") %]</th>
+   <th>[% LxERP.t8("Shop part") %]</th>
+   <th>[% LxERP.t8("Price source") %]</th>
+   <th>[% LxERP.t8("Price") %]</th>
+   <th>[% LxERP.t8("Stock Local/Shop") %]</th>
+   <th>[% LxERP.t8("Last update") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+   <th>[% LxERP.t8("Action") %]</th>
+  </tr>
+  </thead>
+  [%- FOREACH shop_part = SHOP_PARTS %]
+  <tr class="listrow">
+   <td>[% HTML.escape( shop_part.shop.description ) %]</td>
+   <td>[% L.html_tag('span', shop_part.active, id => 'shop_part_active_' _ shop_part.id ) %]</td>
+   <td>[% L.html_tag('span', shop_part.shop_description, id => 'shop_part_description_' _ shop_part.id ) %]</td>
+   <td> [% L.html_tag('span',LxERP.t8(), id => 'active_price_source_' _ shop_part.id) %] </td>
+   <td>[% L.html_tag('span','Price', id => 'price_' _ shop_part.id) %]</td>
+   <td>[% L.html_tag('span','Stock', id => 'stock_' _ shop_part.id) %]</td>
+   <td>[% L.html_tag('span', shop_part.last_update.to_kivitendo('precision' => 'minute'), id => 'shop_part_last_update_' _ shop_part.id ) %]</td>
+   <td>[% L.button_tag("kivi.shop_part.edit_shop_part(" _ shop_part.id _ ")", LxERP.t8("Edit"))  %]</td>
+   <td>[% L.button_tag("kivi.shop_part.update_shop_part(" _ shop_part.id _ ")", LxERP.t8("Upload"))  %]</td>
+   <td>[% L.button_tag("kivi.shop_part.get_all_categories(" _ shop_part.id _ ")", LxERP.t8("Shopcategories"))  %]</td>
+  </tr>
+  <script type="text/javascript">
+    $(function() {
+      kivi.shop_part.update_price_n_price_source([% shop_part.id %],'[% shop_part.active_price_source %]');
+      kivi.shop_part.update_stock([% shop_part.id %]);
+    });
+  </script>
+  [%- END %]
+  [%- FOREACH shop = SHOPS_NOT_ASSIGNED %]
+  <tr>
+   <td>[% HTML.escape( shop.description ) %]</td>
+   <td></td>
+   <td></td>
+   <td></td>
+   <td>[% L.button_tag("kivi.shop_part.create_shop_part(" _ id _ ", " _ shop.id _ ")", LxERP.t8("Add"))  %]</td>
+   <td></td>
+  </tr>
+  [%- END %]
+</table>
+
+
+[% # L.dump(shop_part) %]
+<h2>[% LxERP.t8("Shopvariables and Images - valid for all shops") %]</h2>
+  [%- IF CUSTOM_VARIABLES.size %]
+[%  # L.dump(CUSTOM_VARIABLES) %]
+  <div id="shop_custom_variables">
+   <p>
+    <table>
+     [%- FOREACH var = CUSTOM_VARIABLES %]
+      [%- IF var.name.match('^shop_') %]
+       <tr>
+        [% # IF !var.partsgroup_filtered %]
+          <td align="right" valign="top">[% HTML.escape(var.description) %]</td>
+        [% # END %]
+      <td align="right" valign="top">[% var.VALID_BOX %]</td>
+        <td valign="top">[% var.HTML_CODE %]</td>
+       </tr>
+      [%- END %]
+     [%- END %]
+    </table>
+   </p>
+  </div>
+  [%- END %]
+  <script type="text/javascript">
+    $(function() {
+      kivi.shop_part.show_images([% shop_part.part_id %]);
+    });
+  </script>
+  <div id="shop_images" border=1 >
+  </div>
+</div>
+
diff --git a/templates/webpages/shops/form.html b/templates/webpages/shops/form.html
new file mode 100644
index 000000000..1c3dfad06
--- /dev/null
+++ b/templates/webpages/shops/form.html
@@ -0,0 +1,84 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+
+[% SET style="width: 400px" %]
+[% SET size=34 %]
+
+<h1>[% HTML.escape(title) %]</h1>
+
+<form action="controller.pl" method="post">
+
+[%- INCLUDE 'common/flash.html' %]
+
+[%- L.hidden_tag("id", SELF.shop.id) %]
+
+<table>
+  <tr>
+    <th align="right">[% 'Description' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.description", SELF.shop.description, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Shop type' | $T8 %]</th>
+    <td>[% L.select_tag('shop.connector', SELF.connectors, value_key = 'id', title_key = 'description', with_empty = 1, default = SELF.shop.connector, default_value_key='id' ) %]</td>
+  <tr>
+  <tr>
+    <th align="right">[% 'Price type' | $T8 %]</th>
+    <td>[% L.select_tag('shop.pricetype', SELF.price_types, value_key = 'id', title_key = 'name', with_empty = 1, default = SELF.shop.pricetype, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Price Source' | $T8 %]</th>
+    <td>[% L.select_tag('shop.price_source', SELF.price_sources, value_key = 'id', title_key = 'name', with_empty = 1, default = SELF.shop.price_source, default_value_key='id' ) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'URL' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.url", SELF.shop.url, size=size) %]</td>
+  </tr>
+  <tr>
+    <th align="right">[% 'Port' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.port", SELF.shop.port, size=5) %]</td>
+   <tr>
+    <th align="right">[% 'User' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.login", SELF.shop.login, size=12) %]</td>
+  </tr>
+    <tr>
+    <th align="right">[% 'Password' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.password", SELF.shop.password, size=12) %]</td>
+  </tr>
+ </tr>
+  <tr>
+    <th align="right">[% 'Obsolete' | $T8 %]</th>
+    <td>[% L.checkbox_tag('shop.obsolete', checked = SELF.shop.obsolete, for_submit=1) %]</td>
+  </tr>
+</table>
+
+ <p>
+  [% L.hidden_tag("action", "Shop/dispatch") %]
+  [% L.submit_tag("action_" _  (SELF.shop.id ? "update" : "create"), LxERP.t8('Save'), onclick="return check_prerequisites();") %]
+  [%- IF SELF.shop.id -%]
+    [% L.submit_tag("action_delete", LxERP.t8('Delete')) %]
+  [%- END %]
+  <a href="[% SELF.url_for(action='list') %]">[%- LxERP.t8("Cancel") %]</a>
+ </p>
+
+ <hr>
+
+<script type="text/javascript">
+<!--
+function check_prerequisites() {
+  if ($('#shop_description').val() === "") {
+    alert(kivi.t8('The name is missing.'));
+    return false;
+  }
+  if ($('#shop_url').val() === "") {
+    alert(kivi.t8('The URL is missing.'));
+    return false;
+  }
+  if ($('#shop_port').val() === "") {
+    alert(kivi.t8('The port is missing.'));
+    return false;
+  }
+
+  return true;
+}
+-->
+</script>
+</form>
diff --git a/templates/webpages/shops/list.html b/templates/webpages/shops/list.html
new file mode 100644
index 000000000..4d6ecc76c
--- /dev/null
+++ b/templates/webpages/shops/list.html
@@ -0,0 +1,33 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%][%- INCLUDE 'common/flash.html' %]
+
+<h1>[% title %]</h1>
+
+<p>
+ <table width="100%" id="shop_list">
+  <thead>
+   <tr class="listheading">
+    <th align="center" width="1%"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+    <th>[% 'Description' | $T8 %]</th>
+    <th>[% 'Type' | $T8 %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [%- FOREACH shop = SHOPS %]
+    <tr class="listrow" id="shop_id_[% shop.id %]">
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td><a href="[% SELF.url_for(action='edit', id=shop.id) %]">[% HTML.escape(shop.description) %]</a></td>
+     <td>[% HTML.escape(shop.connector) %]</a></td>
+    </tr>
+   [%- END %]
+  </tbody>
+ </table>
+</p>
+
+<hr height="3">
+
+[% L.sortable_element('#shop_list tbody', url=SELF.url_for(action='reorder'), with='shop_id') %]
+
+<p>
+ <a href="[% SELF.url_for(action='new') %]">[%- 'Add' | $T8 %]</a>
+</p>
