WebshopApi: ShopPart Controller
authorWerner Hahn <wh@futureworldsearch.net>
Fri, 22 Sep 2017 00:33:23 +0000 (02:33 +0200)
committerWerner Hahn <wh@futureworldsearch.net>
Tue, 26 Sep 2017 10:25:02 +0000 (12:25 +0200)
SL/Controller/ShopPart.pm [new file with mode: 0644]
image/gruener_punkt.gif [new file with mode: 0644]
image/roter_punkt.gif [new file with mode: 0644]
js/kivi.ShopPart.js [new file with mode: 0644]
templates/webpages/shop_part/_filter.html [new file with mode: 0644]
templates/webpages/shop_part/_list_articles.html [new file with mode: 0644]
templates/webpages/shop_part/_list_images.html [new file with mode: 0644]
templates/webpages/shop_part/_transfer_status.html [new file with mode: 0644]
templates/webpages/shop_part/_upload_status.html [new file with mode: 0644]
templates/webpages/shop_part/categories.html [new file with mode: 0644]
templates/webpages/shop_part/edit.html [new file with mode: 0644]

diff --git a/SL/Controller/ShopPart.pm b/SL/Controller/ShopPart.pm
new file mode 100644 (file)
index 0000000..fb2fe92
--- /dev/null
@@ -0,0 +1,493 @@
+package SL::Controller::ShopPart;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::BackgroundJob::ShopPartMassUpload;
+use SL::System::TaskServer;
+use Data::Dumper;
+use SL::Locale::String qw(t8);
+use SL::DB::ShopPart;
+use SL::DB::Shop;
+use SL::DB::File;
+use SL::DB::ShopImage;
+use SL::DB::Default;
+use SL::Helper::Flash;
+use SL::Controller::Helper::ParseFilter;
+use MIME::Base64;
+
+use Rose::Object::MakeMethods::Generic
+(
+   scalar                 => [ qw(price_sources) ],
+  'scalar --get_set_init' => [ qw(shop_part file shops) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('add_javascripts', only => [ qw(edit_popup list_articles) ]);
+__PACKAGE__->run_before('load_pricesources',    only => [ qw(create_or_edit_popup) ]);
+
+#
+# actions
+#
+
+sub action_create_or_edit_popup {
+  my ($self) = @_;
+
+  $self->render_shop_part_edit_dialog();
+}
+
+sub action_update_shop {
+  my ($self, %params) = @_;
+
+  my $shop_part = SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
+  die unless $shop_part;
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $shop_part->shop );
+
+  my $connect = $shop->check_connectivity;
+  if($connect->{success}){
+    my $return    = $shop->connector->update_part($self->shop_part, 'all');
+
+    # the connector deals with parsing/result verification, just needs to return success or failure
+    if ( $return == 1 ) {
+      my $now = DateTime->now;
+      my $attributes->{last_update} = $now;
+      $self->shop_part->assign_attributes(%{ $attributes });
+      $self->shop_part->save;
+      $self->js->html('#shop_part_last_update_' . $shop_part->id, $now->to_kivitendo('precision' => 'minute'))
+             ->flash('info', t8("Updated part [#1] in shop [#2] at #3", $shop_part->part->displayable_name, $shop_part->shop->description, $now->to_kivitendo('precision' => 'minute') ) )
+             ->render;
+    } else {
+      $self->js->flash('error', t8('The shop part wasn\'t updated.'))->render;
+    }
+  }else{
+    $self->js->flash('error', t8('The shop part wasn\'t updated. #1', $connect->{data}->{version}))->render;
+  }
+
+
+}
+
+sub action_show_files {
+  my ($self) = @_;
+
+  my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $::form->{id}, ], with_objects => 'file', sort_by => 'position' );
+
+  $self->render('shop_part/_list_images', { header => 0 }, IMAGES => $images);
+}
+
+sub action_ajax_delete_file {
+  my ( $self ) = @_;
+  $self->file->delete;
+
+  $self->js
+    ->run('kivi.ShopPart.show_images',$self->file->object_id)
+    ->render();
+}
+
+sub action_get_categories {
+  my ($self) = @_;
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $self->shop_part->shop );
+
+  my $connect = $shop->check_connectivity;
+  if($connect->{success}){
+    my $categories = $shop->connector->get_categories;
+
+    $self->js
+      ->run(
+        'kivi.ShopPart.shop_part_dialog',
+        t8('Shopcategories'),
+        $self->render('shop_part/categories', { output => 0 }, CATEGORIES => $categories )
+      )
+      ->reinit_widgets;
+      $self->js->render;
+  }else{
+    $self->js->flash('error', t8('Can\'t connect to shop. #1', $connect->{data}->{version}))->render;
+  }
+
+}
+
+sub action_show_price_n_pricesource {
+  my ($self) = @_;
+
+  my ( $price, $price_src_str ) = $self->get_price_n_pricesource($::form->{pricesource});
+
+  if( $price_src_str eq 'sellprice'){
+    $price_src_str = t8('Sellprice');
+  }elsif( $price_src_str eq 'listprice'){
+    $price_src_str = t8('Listprice');
+  }elsif( $price_src_str eq 'lastcost'){
+    $price_src_str = t8('Lastcost');
+  }
+  $self->js->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
+           ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
+           ->render;
+}
+
+sub action_show_stock {
+  my ($self) = @_;
+  my ( $stock_local, $stock_onlineshop, $active_online );
+
+  require SL::Shop;
+  my $shop = SL::Shop->new( config => $self->shop_part->shop );
+
+  if($self->shop_part->last_update) {
+    my $shop_article = $shop->connector->get_article($self->shop_part->part->partnumber);
+    $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
+    $active_online = $shop_article->{data}->{active};
+  }
+
+  $stock_local = $self->shop_part->part->onhand;
+
+  $self->js->html('#stock_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$stock_local,0)."/".$::form->format_amount(\%::myconfig,$stock_onlineshop,0))
+           ->html('#toogle_' . $self->shop_part->id,$active_online)
+           ->render;
+}
+
+sub action_get_n_write_categories {
+  my ($self) = @_;
+
+  my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
+  foreach my $part(@shop_parts){
+
+    my $shop_part = SL::DB::Manager::ShopPart->get_all( where => [id => $part], with_objects => ['part', 'shop'])->[0];
+    require SL::DB::Shop;
+    my $shop = SL::Shop->new( config => $shop_part->shop );
+    my $online_article = $shop->connector->get_article($shop_part->part->partnumber);
+    my $online_cat = $online_article->{data}->{categories};
+    my @cat = ();
+    for(keys %$online_cat){
+      my @cattmp;
+      push @cattmp,$online_cat->{$_}->{id};
+      push @cattmp,$online_cat->{$_}->{name};
+      push @cat,\@cattmp;
+    }
+    my $attributes->{shop_category} = \@cat;
+    my $active->{active} = $online_article->{data}->{active};
+    $shop_part->assign_attributes(%{$attributes}, %{$active});
+    $shop_part->save;
+  }
+  $self->redirect_to( action => 'list_articles' );
+}
+
+sub action_save_categories {
+  my ($self) = @_;
+
+  my @categories =  @{ $::form->{categories} || [] };
+
+    my @cat = ();
+    foreach my $cat ( @categories) {
+      my @cattmp;
+      push( @cattmp,$cat );
+      push( @cattmp,$::form->{"cat_id_${cat}"} );
+      push( @cat,\@cattmp );
+    }
+
+  my $categories->{shop_category} = \@cat;
+
+  my $params = delete($::form->{shop_part}) || { };
+
+  $self->shop_part->assign_attributes(%{ $params });
+  $self->shop_part->assign_attributes(%{ $categories });
+
+  $self->shop_part->save;
+
+  flash('info', t8('The categories has been saved.'));
+
+  $self->js->run('kivi.ShopPart.close_dialog')
+           ->flash('info', t8("Updated categories"))
+           ->render;
+}
+
+sub action_reorder {
+  my ($self) = @_;
+  require SL::DB::ShopImage;
+  SL::DB::ShopImage->reorder_list(@{ $::form->{image_id} || [] });
+
+  $self->render(\'', { type => 'json' });
+}
+
+sub action_list_articles {
+  my ($self) = @_;
+
+  my %filter      = ($::form->{filter} ? parse_filter($::form->{filter}) : query => [ 'shop.obsolete' => 0 ]);
+  my $sort_by     = $::form->{sort_by} ? $::form->{sort_by} : 'part.partnumber';
+  $sort_by .=$::form->{sort_dir} ? ' DESC' : ' ASC';
+
+  my $articles = SL::DB::Manager::ShopPart->get_all( %filter ,with_objects => [ 'part','shop' ], sort_by => $sort_by );
+
+  foreach my $article (@{ $articles}) {
+    my $images = SL::DB::Manager::ShopImage->get_all_count( where => [ 'files.object_id' => $article->part->id, ], with_objects => 'file', sort_by => 'position' );
+    $article->{images} = $images;
+  }
+
+  $self->render('shop_part/_list_articles', title => t8('Webshops articles'), SHOP_PARTS => $articles);
+}
+
+sub action_upload_status {
+  my ($self) = @_;
+  my $job     = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
+  my $html    = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
+
+  $self->js->html('#status_mass_upload', $html);
+  $self->js->run('kivi.ShopPart.massUploadFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::ShopPartMassUpload->DONE();
+  $self->js->render;
+}
+
+sub action_mass_upload {
+  my ($self) = @_;
+
+  my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
+
+  my $job = SL::DB::BackgroundJob->new(
+        type                 => 'once',
+        active               => 1,
+        package_name         => 'ShopPartMassUpload',
+        )->set_data(
+        shop_part_record_ids => [ @shop_parts ],
+        todo                 => $::form->{upload_todo},
+        status               => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
+        conversation         => [ ],
+        num_uploaded         => 0,
+   )->update_next_run_at;
+
+   SL::System::TaskServer->new->wake_up;
+
+   my $html = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
+
+   $self->js
+      ->html('#status_mass_upload', $html)
+      ->run('kivi.ShopPart.massUploadStarted')
+      ->render;
+}
+
+sub action_update {
+  my ($self) = @_;
+
+  $self->create_or_update;
+}
+
+sub render_shop_part_edit_dialog {
+  my ($self) = @_;
+
+  $self->js
+    ->run(
+      'kivi.ShopPart.shop_part_dialog',
+      t8('Shop part'),
+      $self->render('shop_part/edit', { output => 0 })
+    )
+    ->reinit_widgets;
+
+  $self->js->render;
+}
+
+sub create_or_update {
+  my ($self) = @_;
+
+  my $is_new = !$self->shop_part->id;
+
+  my $params = delete($::form->{shop_part}) || { };
+
+  $self->shop_part->assign_attributes(%{ $params });
+
+  $self->shop_part->save;
+
+  my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
+if(!$is_new){
+  flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
+  $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
+           ->html('#shop_part_active_' . $self->shop_part->id, $self->shop_part->active)
+           ->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
+           ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
+           ->run('kivi.ShopPart.close_dialog')
+           ->flash('info', t8("Updated shop part"))
+           ->render;
+         }else{
+    $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->shop_part->part_id);
+  }
+}
+
+#
+# internal stuff
+#
+sub add_javascripts  {
+  $::request->{layout}->add_javascripts(qw(kivi.ShopPart.js));
+}
+
+sub load_pricesources {
+  my ($self) = @_;
+
+  my $pricesources;
+  push( @{ $pricesources } , { id => "master_data/sellprice", name => t8("Master Data")." - ".t8("Sellprice") },
+                             { id => "master_data/listprice", name => t8("Master Data")." - ".t8("Listprice") },
+                             { id => "master_data/lastcost",  name => t8("Master Data")." - ".t8("Lastcost") }
+                             );
+  my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
+  foreach my $pg ( @$pricegroups ) {
+    push( @{ $pricesources } , { id => "pricegroup/".$pg->id, name => t8("Pricegroup") . " - " . $pg->pricegroup} );
+  };
+
+  $self->price_sources( $pricesources );
+}
+
+sub get_price_n_pricesource {
+  my ($self,$pricesource) = @_;
+
+  my ( $price_src_str, $price_src_id ) = split(/\//,$pricesource);
+
+  require SL::DB::Pricegroup;
+  require SL::DB::Part;
+  my $price;
+  if ($price_src_str eq "master_data") {
+    my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id );
+    $price         = $part->$price_src_id;
+    $price_src_str = $price_src_id;
+    }else{
+    my $part       = SL::DB::Manager::Part->get_all( where => [id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id], with_objects => ['prices'],limit => 1)->[0];
+    #my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id );
+    my $pricegrp   = SL::DB::Manager::Pricegroup->find_by( id => $price_src_id )->pricegroup;
+    $price         = $part->prices->[0]->price;
+    $price_src_str = $pricegrp;
+  }
+  return($price,$price_src_str);
+}
+
+sub check_auth {
+  $::auth->assert('shop_part_edit');
+}
+
+sub init_shop_part {
+  if ($::form->{shop_part_id}) {
+    SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
+  } else {
+    SL::DB::ShopPart->new(shop_id => $::form->{shop_id}, part_id => $::form->{part_id});
+  };
+}
+
+sub init_file {
+  my $file = $::form->{id} ? SL::DB::File->new(id => $::form->{id})->load : SL::DB::File->new;
+  return $file;
+}
+
+sub init_shops {
+  SL::DB::Shop->shops_dd;
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+
+=head1 NAME
+
+SL::Controller::ShopPart - Controller for managing ShopParts
+
+=head1 SYNOPSIS
+
+ShopParts are configured in a tab of the corresponding part.
+
+=head1 ACTIONS
+
+=over 4
+
+=item C<action_update_shop>
+
+To be called from the "Update" button of the shoppart, for manually syncing/upload one part with its shop. Calls some ClientJS functions to modifiy original page.
+
+=item C<action_show_files>
+
+
+
+=item C<action_ajax_delete_file>
+
+
+
+=item C<action_get_categories>
+
+
+
+=item C<action_show_price_n_pricesource>
+
+
+
+=item C<action_show_stock>
+
+
+
+=item C<action_get_n_write_categories>
+
+Can be used to sync the categories of a shoppart with the categories from online.
+
+=item C<action_save_categories>
+
+The ShopwareConnector works with the CategoryID @categories[x][0] in others/new Connectors it must be tested
+Each assigned categorie is saved with id,categorie_name an multidimensional array and could be expanded with categoriepath or what is needed
+
+=item C<action_reorder>
+
+
+
+=item C<action_upload_status>
+
+
+
+=item C<action_mass_upload>
+
+
+
+=item C<action_update>
+
+
+
+=item C<create_or_update>
+
+
+
+=item C<render_shop_part_edit_dialog>
+
+when self->shop_part is called in template, it will be an existing shop_part with id,
+or a new shop_part with only part_id and shop_id set
+
+=item C<add_javascripts>
+
+
+=item C<load_pricesources>
+
+the price sources to use for the article: sellprice, lastcost,
+listprice, or one of the pricegroups. It overwrites the default pricesource from the shopconfig.
+TODO: implement valid pricerules for the article
+
+=item C<get_price_n_pricesource>
+
+
+=item C<check_auth>
+
+
+=item C<init_shop_part>
+
+
+=item C<init_file>
+
+
+=item C<init_shops>
+
+data for drop down filter options
+
+=back
+
+=head1 TODO
+
+CheckAuth
+Pricesrules, pricessources aren't fully implemented yet.
+
+=head1 AUTHORS
+
+G. Richardson E<lt>information@kivitendo-premium.deE<gt>
+W. Hahn E<lt>wh@futureworldsearch.netE<gt>
+
+=cut
diff --git a/image/gruener_punkt.gif b/image/gruener_punkt.gif
new file mode 100644 (file)
index 0000000..fe6c55d
Binary files /dev/null and b/image/gruener_punkt.gif differ
diff --git a/image/roter_punkt.gif b/image/roter_punkt.gif
new file mode 100644 (file)
index 0000000..2331af4
Binary files /dev/null and b/image/roter_punkt.gif differ
diff --git a/js/kivi.ShopPart.js b/js/kivi.ShopPart.js
new file mode 100644 (file)
index 0000000..64a1e46
--- /dev/null
@@ -0,0 +1,151 @@
+namespace('kivi.ShopPart', function(ns) {
+  var $dialog;
+
+  ns.shop_part_dialog = function(title, html) {
+    var id            = 'jqueryui_popup_dialog';
+    var dialog_params = {
+      id:     id,
+      width:  800,
+      height: 500,
+      modal:  true,
+      close: function(event, ui) { $dialog.remove(); },
+    };
+
+    $('#' + id).remove();
+
+    $dialog = $('<div style="display:none" id="' + id + '"></div>').appendTo('body');
+    $dialog.attr('title', title);
+    $dialog.html(html);
+    $dialog.dialog(dialog_params);
+
+    $('.cancel').click(ns.close_dialog);
+
+    return true;
+  };
+
+  ns.close_dialog = function() {
+    $dialog.dialog("close");
+  }
+
+  ns.save_shop_part = function(shop_part_id) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/update' }
+             , { name: 'shop_part_id',  value: shop_part_id }
+    );
+
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.add_shop_part = function(part_id,shop_id) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/update' }
+    );
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.edit_shop_part = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/create_or_edit_popup', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.create_shop_part = function(part_id, shop_id) {
+    $.post('controller.pl', { action: 'ShopPart/create_or_edit_popup', part_id: part_id, shop_id: shop_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.get_all_categories = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/get_categories', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.save_categories = function(shop_part_id, shop_id) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'ShopPart/save_categories' }
+             , { name: 'shop_id', value: shop_id }
+             , { name: 'shop_part_id', value: shop_part_id }
+    );
+
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_shop_part = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/update_shop', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_discount_source = function(row, source, discount_str) {
+    $('#active_discount_source_' + row).val(source);
+    if (discount_str) $('#discount_' + row).val(discount_str);
+    $('#update_button').click();
+  }
+
+  ns.show_images = function(id) {
+    var url = 'controller.pl?action=ShopPart/show_files&id='+id;
+    $('#shop_images').load(url);
+  }
+
+  ns.update_price_n_price_source = function(shop_part_id,price_source) {
+    $.post('controller.pl', { action: 'ShopPart/show_price_n_pricesource', shop_part_id: shop_part_id, pricesource: price_source }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_stock = function(shop_part_id) {
+    $.post('controller.pl', { action: 'ShopPart/show_stock', shop_part_id: shop_part_id }, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.massUploadInitialize = function() {
+    kivi.popup_dialog({
+      id: 'status_mass_upload',
+      dialog: {
+        title: kivi.t8('Status Shopupload')
+      }
+    });
+  };
+
+  ns.massUploadStarted = function() {
+    $('#status_mass_upload').data('timerId', setInterval(function() {
+      $.get("controller.pl", {
+        action: 'ShopPart/upload_status',
+        job_id: $('#smu_job_id').val()
+      }, kivi.eval_json_result);
+    }, 5000));
+  };
+
+  ns.massUploadFinished = function() {
+    clearInterval($('#status_mass_upload').data('timerId'));
+    $('.ui-dialog-titlebar button.ui-dialog-titlebar-close').prop('disabled', '')
+  };
+
+  ns.imageUpload = function(id,type,filetype,upload_title,gl) {
+    kivi.popup_dialog({ url:     'controller.pl',
+                        data:    { action: 'File/ajax_upload',
+                                   file_type:   filetype,
+                                   object_type: type,
+                                   object_id:   id,
+                                   is_global:   gl
+                                 },
+                        id:     'files_upload',
+                        dialog: { title: kivi.t8('File upload'), width: 650, height: 240 } });
+    return true;
+  }
+
+
+  ns.setup = function() {
+    kivi.ShopPart.massUploadInitialize();
+    kivi.submit_ajax_form('controller.pl?action=ShopPart/mass_upload','[name=shop_parts]');
+  };
+
+});
diff --git a/templates/webpages/shop_part/_filter.html b/templates/webpages/shop_part/_filter.html
new file mode 100644 (file)
index 0000000..3e49a0e
--- /dev/null
@@ -0,0 +1,28 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' metdod='post' id="shop_part_filter">
+  [% L.hidden_tag('filter.shop.obsolete', 0) %]
+ <table id='filter_table'>
+
+    <tr>
+     <td>[% 'Shop' | $T8 %]</td>
+     <td>[% L.select_tag('filter.shop_id:eq_ignore_empty', SELF.shops, value_key = 'value', title_key = 'title', default=0) %]</td>
+    </tr>
+    <tr>
+     <td>[% 'Part marked as "Shop article"' %]
+     <td>[% L.yes_no_tag('filter.part.shop', FORM.filter.part.shop, default='1', with_empty=1, empty_title='---') %]</td>
+    </tr>
+
+ </table>
+
+ <p>
+  <a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+  <br>
+ </p>
+ <p>
+   [% L.hidden_tag('action', 'ShopPart/dispatch') %]
+   [% L.submit_tag('action_list_articles',LxERP.t8('renew')) %]
+ </p>
+</form>
diff --git a/templates/webpages/shop_part/_list_articles.html b/templates/webpages/shop_part/_list_articles.html
new file mode 100644 (file)
index 0000000..244fb76
--- /dev/null
@@ -0,0 +1,136 @@
+[%- USE HTML -%][%- USE LxERP -%][%- USE L -%][%- USE T8 -%]
+[% USE Dumper %]
+
+
+<h1>[% title %]</h1>
+[%- PROCESS 'shop_part/_filter.html' filter=SELF.models.filtered.laundered %]
+<hr>
+<form method="post" action="controller.pl" name="shop_parts" id="shopparts">
+  <div class="data_count">[% 'Number of Data: ' | $T8 %] [% SHOP_PARTS.size %]</div>
+  <table id="shoplist" width="100%" >
+    <thead>
+      <tr class="listheading">
+      <th>[% L.checkbox_tag('check_all') %]</th>
+    <th>[% IF FORM.sort_by == 'shop.description' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=shop.description&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Shop Host/Connector' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=shop.description&sort_dir=0" class="sort_link">
+        [% 'Shop Host/Connector' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'part.partnumber' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partnumber&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Partnumber' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partnumber&sort_dir=0" class="sort_link">
+        [% 'Partnumber' | $T8 %]</a>
+    [% END %]
+    </th>
+    <th>[% IF FORM.sort_by == 'part.description' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.description&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Description' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.description&sort_dir=0" class="sort_link">
+        [% 'Description' | $T8 %]</a>
+    [% END %]
+    </th>
+      <th>[% 'Info' | $T8 %]</th>
+      <th>[% 'Active' | $T8 %]</th>
+      <th>[% 'Price source' | $T8 %]</th>
+      <th>[% 'Price' | $T8 %]</th>
+    <th>[% IF FORM.sort_by == 'part.onhand' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.onhand&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Stock Local/Shop' | $T8 %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.onhand&sort_dir=0" class="sort_link">
+        [% 'Stock Local/Shop' | $T8 %]</a>
+    [% END %]
+    </th>
+      <th>[% 'Last update' | $T8 %]</th>
+      <th>[% 'Images' | $T8 %]</th>
+    <th>[% IF FORM.sort_by == 'part.partsgroup_id' %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partsgroup_id&sort_dir=[% 1 - FORM.sort_dir %]" class="sort_link">
+        [% 'Category' %][% IF FORM.sort_dir == 0 %]<img border="0" src="image/down.png">[% ELSE %]<img border="0" src="image/up.png">[% END %]</a>
+    [% ELSE %]
+      <a href ="controller.pl?action=ShopPart/list_articles&sort_by=part.partsgroup_id&sort_dir=0" class="sort_link">
+        [% 'Category' | $T8 %]</a>
+    [% END %]
+    </th>
+    </tr>
+  </thead>
+  [%- FOREACH shop_part = SHOP_PARTS %]
+  [%- # IF shop_part.shop.obsolete %]
+    <tr class="listrow">
+      <td>[% L.checkbox_tag('shop_parts_ids[]', checked=0, value=shop_part.id) %]</td>
+      <td>[% HTML.escape( shop_part.shop.description ) %]/[% HTML.escape( shop_part.shop.connector ) %]</td>
+      <td>[% HTML.escape( shop_part.part.partnumber ) %]</td>
+      <td><a href="controller.pl?part.id=[% shop_part.part.id %]&action=Part/edit&callback=[% HTML.url('controller.pl?action=ShopPart/list_articles') %]#shop_variables">[% HTML.escape( shop_part.part.description ) %]</a></td>
+      <td>
+        [% IF shop_part.shop_description %]
+          [% 'Info' | $T8 %]
+        [% ELSE %]
+          [% 'No Shopdescription' | $T8 %]
+        [% END %]
+      </td>
+      <td style="vertical-align:middle;text-align:center;">
+        [% IF shop_part.active %]
+        <div id="toogle_[% shop_part.id %]" style="background-image:url(image/gruener_punkt.gif);background-repeat:no-repeat;witdh:15px;height:15px;">&nbsp; </div>
+        [% ELSE %]
+        <div id="toogle_[% shop_part.id %]" style="background-image:url(image/roter_punkt.gif);background-repeat:no-repeat;witdh:15px;height:15px;">&nbsp; </div>
+        [% END %]
+      </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>
+        [% IF shop_part.images %]
+          [% shop_part.images %]
+        [% ELSE %]
+          [% 'No Shopimages' | $T8 %]
+        [% END %]
+      </td>
+      <td>
+        [% IF shop_part.shop_category %]
+          [% IF shop_part.shop_category.1.size > 1%]
+            [% FOREACH cat = shop_part.shop_category %]
+              [% HTML.escape(cat.1) %]<br>
+            [% END %]
+          [% ELSE %]
+            [% HTML.escape(shop_part.shop_category.1) %]<br>
+          [% END %]
+        [% END %]
+      </td>
+    <script type="text/javascript">
+      $(function() {
+         kivi.ShopPart.update_price_n_price_source([% shop_part.id %],'[% shop_part.active_price_source %]');
+         kivi.ShopPart.update_stock([% shop_part.id %]);
+      });
+    </script>
+    </tr>
+    [%- # END %]
+  [%- END %]
+</table>
+
+  <hr>
+  <div>
+    [% L.radio_button_tag('upload_todo', value='all', label= LxERP.t8('All Data')) %]
+    [% L.radio_button_tag('upload_todo', value='price', label= LxERP.t8('Only Price')) %]
+    [% L.radio_button_tag('upload_todo', value='stock', label= LxERP.t8('Only Stock')) %]
+    [% L.radio_button_tag('upload_todo', value='price_stock', checked=1, label= LxERP.t8('Price and Stock')) %]
+    [% L.button_tag("kivi.ShopPart.setup();", LxERP.t8("Upload all marked"), id="mass_transfer") %]
+  </div>
+  <div id="status_mass_upload" style="display: none;">
+    [%- INCLUDE 'shop_part/_upload_status.html' %]
+  </div>
+</form>
+<hr>
+<script type="text/javascript">
+<!--
+
+$(function() {
+  $('#check_all').checkall('INPUT[name^="shop_parts_ids"]');
+});
+-->
+</script>
diff --git a/templates/webpages/shop_part/_list_images.html b/templates/webpages/shop_part/_list_images.html
new file mode 100644 (file)
index 0000000..6c811b3
--- /dev/null
@@ -0,0 +1,35 @@
+[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%]
+[%- USE T8 %][% USE Base64 %]
+[%- USE Dumper %]
+<table width="100%" id="images_list">
+  <thead>
+    <tr class="listheading">
+      <th width="10px"><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop"></th>
+      <th width="70px"></th>
+      <th>[% 'Title' | $T8 %]</th>
+      <th>[% 'Description' | $T8 %]</th>
+      <th>[% 'Filename' | $T8 %]</th>
+      <th>[% 'Orig. Size w/h' | $T8 %]</th>
+      <th>[% 'Action' | $T8 %]</th>
+    </tr>
+  </thead>
+  <tbody>
+   [%-  FOREACH img = IMAGES %]
+   [% # Dumper.dump_html(img) %]
+    <tr class="listrow" id="image_id_[%  img.id %]">
+      <td><img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop"></td>
+      <td width="70px"><img src="data:[%  img.thumbnail_content_type %];base64,[%  img.thumbnail_content.encode_base64 %]" alt="[%  img.file.title %]"></td>
+      <td>[% HTML.escape(img.file.title) %]</td>
+      <td>[% HTML.escape(img.file.description) %]</td>
+      <td>[% HTML.escape(img.file.file_name) %]</td>
+      <td>[% HTML.escape(img.org_file_width) _  ' x ' _ HTML.escape(img.org_file_height) %]</td>
+      <td>[% L.button_tag("kivi.File.delete_file(" _ img.file_id _ ", 'ShopPart/ajax_delete_file')", LxERP.t8('Delete'), confirm=LxERP.t8("Are you sure?")) %]</td>
+    </tr>
+   [%  END %]
+  </tbody>
+</table>
+
+[% L.sortable_element('#images_list tbody', url=SELF.url_for(action='reorder'), with='image_id') %]
+<p>
+[% L.button_tag("kivi.ShopPart.imageUpload(" _ FORM.id _ ",'shop_image','image', '',0);", LxERP.t8('File upload') ) %]
+</p>
diff --git a/templates/webpages/shop_part/_transfer_status.html b/templates/webpages/shop_part/_transfer_status.html
new file mode 100644 (file)
index 0000000..bbee02b
--- /dev/null
@@ -0,0 +1,52 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[%- USE Dumper -%]
+[% SET data = job.data_as_hash %]
+
+<h2>[% LxERP.t8("Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="smu_job_id") %]
+
+JOBID: [% job.id %] <p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+<p>
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Uploading Data") %]
+    [% ELSE %]
+     [% LxERP.t8("Done.") %]
+    [% END %]
+   </td>
+  </tr>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of data uploaded:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_uploaded) %] / [% HTML.escape(data.record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Conversion:") %]</th>
+   <td valign="top">
+[% IF !data.status %]
+  –
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Part") %]</th>
+      <th>[% LxERP.t8("Partnumber") %]</th>
+      <th>[% LxERP.t8("Message") %]</th>
+     </tr>
+
+ [% FOREACH message = data.conversion %]
+     <tr>
+      <td valign="top">[% IF error.id %][% L.link(SELF.url_for(controller='ShopOrder', action='show', id=error.id), HTML.escape(error.number), target="_blank") %][% ELSE %]–[% END %]</td>
+      <td valign="top">[% HTML.escape(message.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+ [% END %]
+ </table>
+</p>
diff --git a/templates/webpages/shop_part/_upload_status.html b/templates/webpages/shop_part/_upload_status.html
new file mode 100644 (file)
index 0000000..e7285a2
--- /dev/null
@@ -0,0 +1,51 @@
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
+[%- USE Dumper -%]
+[% SET data = job.data_as_hash %]
+
+<h2>[% LxERP.t8("Watch status") %]</h2>
+
+[% L.hidden_tag('', job.id, id="smu_job_id") %]
+
+JOBID: [% job.id %] <p>
+ [% LxERP.t8("This status output will be refreshed every five seconds.") %]
+</p>
+<p>
+ <table>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Current status:") %]</th>
+   <td valign="top">
+    [% IF !data.status %]
+     [% LxERP.t8("waiting for job to be started") %]
+    [% ELSIF data.status == 1 %]
+     [% LxERP.t8("Uploading Data") %]
+    [% ELSE %]
+     [% LxERP.t8("Done.") %]
+    [% END %]
+   </td>
+  </tr>
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Number of data uploaded:") %]</th>
+   <td valign="top">[% IF data.status > 0 %][% HTML.escape(data.num_uploaded) %] / [% HTML.escape(data.shop_part_record_ids.size) %][% ELSE %]–[% END %]</td>
+  </tr>
+
+  <tr>
+   <th valign="top" align="left">[% LxERP.t8("Conversion:") %]</th>
+   <td valign="top">
+  –
+    <table>
+     <tr class="listheader">
+      <th>[% LxERP.t8("Part") %]</th>
+      <th>[% LxERP.t8("Partnumber") %]</th>
+      <th>[% LxERP.t8("Message") %]</th>
+     </tr>
+
+ [% FOREACH message = data.conversion %]
+     <tr>
+      <td valign="top">[% HTML.escape(message.id) %]</td>
+      <td valign="top">[% HTML.escape(message.number) %]</td>
+      <td valign="top">[% HTML.escape(message.message) %]</td>
+     </tr>
+ [% END %]
+    </table>
+ </table>
+</p>
diff --git a/templates/webpages/shop_part/categories.html b/templates/webpages/shop_part/categories.html
new file mode 100644 (file)
index 0000000..80676eb
--- /dev/null
@@ -0,0 +1,55 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L -%]
+[%- USE P -%]
+[%- USE LxERP -%]
+[%- USE Dumper -%]
+
+[%  LxERP.t8("Part") %]: [% HTML.escape(SELF.shop_part.part.displayable_name) %]<br>
+[%  LxERP.t8("Shop") %]: [% HTML.escape(SELF.shop_part.shop.description) %]
+<br>
+[% # Dumper.dump_html(SELF.shop_part.shop_category) %]
+<br>
+[% # Dumper.dump_html(CATEGORIES) %]
+
+<form action="controller.pl" method="post">
+  [% BLOCK recurse %]
+    [% # path = '' %]<!-- TODO: Pfad wg neuer Kategorie im Shop anlegen -->
+    [% FOREACH obj = data %]
+      <ul>
+        <li>
+        [% checked = '' %]
+        [% # path = path _ obj.name %]
+        [% # test = path.split('\|') %]
+        [% # Dumper.dump_html(SELF.shop_part.shop_category) %]
+        [% IF SELF.shop_part.shop_category.1.grep(obj.name).size %]
+          [% checked = 'checked' %]
+        [% ELSE %]
+          [% FOREACH cat_row = SELF.shop_part.shop_category %]
+            [% IF cat_row.1.grep(obj.name).size %]
+              [% checked = 'checked' %]
+            [% END %]
+          [% END %]
+        [% END %]
+          [% L.checkbox_tag('categories[]',value=obj.id, checked=checked) %][% HTML.escape(obj.name) %][% L.hidden_tag("cat_id_" _ obj.id, obj.name) %]</li>
+        [% IF obj.childrenCount >= 1 %]
+          [% # path = path _ '|' %]
+          [% INCLUDE recurse data=obj.children %]
+        [% END %]
+      </ul>
+    [% END %]
+  [% END %]
+  <div><h2>[% LxERP.t8("Shopcategories") %]</h2>
+      [% FOREACH row = CATEGORIES %]
+<!-- TODO: Is still hardcoded 'Root' is shopware specified -->
+        [% IF row.name == 'Root' %]
+          [% IF row.childrenCount >= 1 %]
+            [% path = '' %]
+            [% INCLUDE recurse data=row.children path=path %]
+          [% END %]
+        [% END %]
+      [% END %]
+  </div>
+    [% L.button_tag("kivi.ShopPart.save_categories(" _ SELF.shop_part.id _", " _ SELF.shop_part.shop.id _")", LxERP.t8("Save"))  %]</td>
+</form>
+
diff --git a/templates/webpages/shop_part/edit.html b/templates/webpages/shop_part/edit.html
new file mode 100644 (file)
index 0000000..418a632
--- /dev/null
@@ -0,0 +1,81 @@
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE L -%]
+[%- USE P -%]
+[%- USE LxERP -%]
+[%- USE Dumper -%]
+
+<p>
+[% LxERP.t8("Part") %]: [% HTML.escape(SELF.shop_part.part.displayable_name) %]<br>
+[% LxERP.t8("Shop") %]: [% HTML.escape(SELF.shop_part.shop.description) %]
+<p>
+[% # Dumper.dump_html(SELF) %]
+<form action="controller.pl" method="post">
+  <div>
+    [% IF SELF.shop_part.id %]
+    [%- L.hidden_tag("shop_part.id", SELF.shop_part.id) %]
+    [%- L.hidden_tag("shop_part.shop_id", SELF.shop_part.shop_id) %]
+    [% ELSE %]
+    [%- L.hidden_tag("shop_part.shop_id", FORM.shop_id) %]
+    [%- L.hidden_tag("shop_part.part_id", FORM.part_id) %]
+    [% END %]
+
+    <table>
+    <tr>
+     <td>[% LxERP.t8("Description") %]</td>
+     <td colspan="3">[% L.textarea_tag('shop_part.shop_description', SELF.shop_part.shop_description, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]</td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Active") %]</td>
+     <td>[% L.yes_no_tag("shop_part.active", SELF.shop_part.active, default = "yes") %]</td>
+     <td>[% LxERP.t8("Date") %]</td>
+     <td>[% L.date_tag("shop_part.show_date", SELF.shop_part.show_date) %]</td>
+    </tr>
+    <tr>
+      <td>[% 'Price Source' | $T8 %]</th>
+      [% IF SELF.shop_part.active_price_source %]
+        [% SET price_source = SELF.shop_part.active_price_source %]
+      [% ELSE %]
+        [% SET price_source = SELF.shop_part.shop.price_source %]
+      [% END %]
+      <td>[% L.select_tag('shop_part.active_price_source', SELF.price_sources, value_key = 'id', title_key = 'name', with_empty = 0, default = price_source, default_value_key='id' ) %]</td>
+     <td>[% LxERP.t8("Front page") %]</td>
+     <td>[% L.yes_no_tag('shop_part.front_page', SELF.shop_part.front_page) %]</td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Sort order") %]</td>
+     <td>[% L.input_tag("shop_part.sortorder", SELF.shop_part.sortorder, size=2) %]</td>
+     <td>[% LxERP.t8("Meta tag title") %]</td>
+     <td>[% L.input_tag("shop_part.metatag_title", SELF.shop_part.metatag_title, size=12) %]</td>
+    </tr>
+    <tr>
+     <td>[% LxERP.t8("Meta tag keywords") %]</td>
+     <td>[% L.input_tag("shop_part.metatag_keywords", SELF.shop_part.metatag_keywords, size=22) %]</td>
+     <td>[% LxERP.t8("Meta tag description") %]</td>
+     <td>[% L.textarea_tag("shop_part.metatag_description", SELF.shop_part.metatag_description, rows=4) %]</td>
+    </tr>
+    </table>
+    [% # L.dump(SELF.shop_part) %]
+
+    [% IF SELF.shop_part.id %]
+    [% L.button_tag("kivi.ShopPart.save_shop_part(" _ SELF.shop_part.id _ ")", LxERP.t8("Save"))  %]</td>
+    [% ELSE %]
+    [% L.button_tag("kivi.ShopPart.add_shop_part(" _ FORM.part_id _", " _ FORM.shop_id _")", LxERP.t8("Save"))  %]</td>
+    [% END %]
+    [% # L.button_tag("kivi.ShopPart.update_partnumber()", LxERP.t8("Update Partnumber"))  %]</td>
+
+    [% # L.hidden_tag("action", "ShopPart/dispatch") %]
+    [% # L.submit_tag('action_update', LxERP.t8('Save')) %]
+
+
+  </div>
+</form>
+
+
+
+[%- IF SELF.shop_part.part.image && INSTANCE_CONF.get_parts_show_image %]
+         <a href="[% SELF.shop_part.part.image | html %]" target="_blank"><img style="[% INSTANCE_CONF.get_parts_image_css %]" src="[% SELF.shop_part.part.image | html %]"/></a>
+[%- END %]
+
+[% # SELF.shop_part.shop_description %]
+[% # L.dump(SELF.shop_part) %]