PriceSource: Preisselektion auf Popup umgestellt.
authorSven Schöling <s.schoeling@linet-services.de>
Tue, 29 Jul 2014 15:36:40 +0000 (17:36 +0200)
committerSven Schöling <s.schoeling@linet-services.de>
Thu, 18 Dec 2014 15:18:21 +0000 (16:18 +0100)
- Logik für geänderte Preise implementiert
- Visualisierung verbessert
- fix für emptied rows
- nachricht wenn invalid und missing
- benachrichtigung für höher/niedriger
- js ausgelagert
- best price benachrichtigung

noch offene bugs:
- preise mit mehr als 2 stellen werden abgeschnitten
- interaktive preise noch nicht möglich
- symbol für "besser preis" ist nicht schön
- beide make_record_item implementierungen sind leicht unterschiedlich
- pricesource controller grösstenteils ungetestet
- performance ist im moment mies

13 files changed:
SL/Controller/PriceSource.pm [new file with mode: 0644]
SL/PriceSource.pm
SL/PriceSource/Base.pm
bin/mozilla/do.pl
bin/mozilla/io.pl
bin/mozilla/ir.pl
bin/mozilla/is.pl
bin/mozilla/oe.pl
js/kivi.io.js [new file with mode: 0644]
locale/de/all
templates/webpages/oe/_price_sources_row.html [deleted file]
templates/webpages/oe/price_sources_dialog.html [new file with mode: 0644]
templates/webpages/oe/sales_order.html

diff --git a/SL/Controller/PriceSource.pm b/SL/Controller/PriceSource.pm
new file mode 100644 (file)
index 0000000..baed7d4
--- /dev/null
@@ -0,0 +1,167 @@
+package SL::Controller::PriceSource;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use List::MoreUtils qw(any uniq apply);
+use SL::ClientJS;
+use SL::Locale::String qw(t8);
+use SL::PriceSource;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(record_item) ],
+ 'scalar --get_set_init' => [ qw(js record) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+#
+# actions
+#
+
+sub action_price_popup {
+  my ($self) = @_;
+
+  my $record_item = _make_record_item($::form->{row});
+
+  $self->render_price_dialog($record_item);
+}
+
+sub render_price_dialog {
+  my ($self, $record_item) = @_;
+
+  my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->record);
+
+  $self->js
+    ->run(
+      'kivi.io.price_chooser_dialog',
+      t8('Available Prices'),
+      $self->render('oe/price_sources_dialog', { output => 0 }, price_source => $price_source)
+    )
+    ->reinit_widgets;
+
+#   if (@errors) {
+#     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
+#     $self->js->show('#dialog_flash_error');
+#   }
+
+  $self->js->render($self);
+}
+
+
+#
+# internal stuff
+#
+
+sub check_auth {
+  $::auth->assert('edit_prices');
+}
+
+sub init_js {
+  SL::ClientJS->new
+}
+
+sub init_record {
+  _make_record();
+}
+
+sub _make_record_item {
+  my ($row) = @_;
+
+  my $class = {
+    sales_order             => 'OrderItem',
+    purchase_oder           => 'OrderItem',
+    sales_quotation         => 'OrderItem',
+    request_quotation       => 'OrderItem',
+    invoice                 => 'InvoiceItem',
+    purchase_invoice        => 'InvoiceItem',
+    purchase_delivery_order => 'DeliveryOrderItem',
+    sales_delivery_order    => 'DeliveryOrderItem',
+  }->{$::form->{type}};
+
+  return unless $class;
+
+  $class = 'SL::DB::' . $class;
+
+  eval "require $class";
+
+  my $obj = $::form->{"orderitems_id_$row"}
+          ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{"orderitems_id_$row"})
+          : $class->new;
+
+  for my $method (apply { s/_$row$// } grep { /_$row$/ } keys %$::form) {
+    next unless $obj->meta->column($method);
+    if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
+      $obj->${\"$method\_as_date"}($::form->{"$method\_$row"});
+    } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
+      $obj->${\"$method\_as_number"}($::form->{"$method\_$row"});
+    } else {
+      $obj->$method($::form->{"$method\_$row"});
+    }
+  }
+
+  if ($::form->{"id_$row"}) {
+    $obj->part(SL::DB::Part->load_cached($::form->{"id_$row"}));
+  }
+
+  return $obj;
+}
+
+sub _make_record {
+  my ($with_items) = @_;
+
+  my $class = {
+    sales_order             => 'Order',
+    purchase_oder           => 'Order',
+    sales_quotation         => 'Order',
+    request_quotation       => 'Order',
+    purchase_delivery_order => 'DeliveryOrder',
+    sales_delivery_order    => 'DeliveryOrder',
+  }->{$::form->{type}};
+
+  if ($::form->{type} eq 'invoice') {
+    $class = $::form->{vc} eq 'customer' ? 'Invoice'
+           : $::form->{vc} eq 'vendor'   ? 'PurchaseInvoice'
+           : do { die 'unknown invoice type' };
+  }
+
+  return unless $class;
+
+  $class = 'SL::DB::' . $class;
+
+  eval "require $class";
+
+  my $obj = $::form->{id}
+          ? $class->meta->convention_manager->auto_manager_class_name->find_by(id => $::form->{id})
+          : $class->new;
+
+  for my $method (keys %$::form) {
+    next unless $obj->can($method);
+    next unless $obj->meta->column($method);
+
+    if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
+      $obj->${\"$method\_as_date"}($::form->{$method});
+    } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
+      $obj->${\"$method\_as\_number"}($::form->{$method});
+    } else {
+      $obj->$method($::form->{$method});
+    }
+  }
+
+  if ($with_items) {
+    my @items;
+    for my $i (1 .. $::form->{rowcount}) {
+      next unless $::form->{"id_$i"};
+      push @items, _make_record_item($i)
+    }
+
+    $obj->items(@items) if @items;
+  }
+
+  return $obj;
+}
+
+1;
+
index 470182c..5237003 100644 (file)
@@ -59,7 +59,7 @@ SL::PriceSource - mixin for price_sources in record items
 PriceSource is an interface that allows generic algorithms to be plugged
 together to calculate available prices for a position in a record.
 
-Each algorithm can access details of the record to realize dependancies on
+Each algorithm can access details of the record to realize dependencies on
 part, customer, vendor, date, quantity etc, which was previously not possible.
 
 =head1 BACKGROUND AND PHILOSOPY
@@ -111,7 +111,7 @@ trying to be smart. The second and third one ensure that later on the
 calculation can be repeated so that invalid prices can be caught (because for
 example the special offer is no longer valid), and so that sales personnel have
 information about rising or falling prices. The fourth point ensures that
-insular calculation processes can be developed independant of the core code.
+insular calculation processes can be developed independent of the core code.
 
 =head1 INTERFACE METHODS
 
index b095684..261897d 100644 (file)
@@ -183,6 +183,8 @@ The price field in purchase records is still C<sellprice>.
 
 C<source> and C<spec> are tainted. If you store data directly in C<spec>, sanitize.
 
+=back
+
 =head1 SEE ALSO
 
 L<SL::PriceSource>,
index 1bda974..b242acb 100644 (file)
@@ -323,7 +323,7 @@ sub form_header {
 
   $form->{follow_up_trans_info} = $form->{donumber} .'('. $follow_up_vc .')';
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
 
   $form->header();
   # Fix für Bug 1082 Erwartet wird: 'abteilungsNAME--abteilungsID'
index c7a06af..ecf7723 100644 (file)
@@ -44,6 +44,7 @@ use List::Util qw(min max first);
 use SL::CVar;
 use SL::Common;
 use SL::CT;
+use SL::Locale::String qw(t8);
 use SL::IC;
 use SL::IO;
 use SL::PriceSource;
@@ -325,8 +326,14 @@ sub display_row {
     if ($form->{"id_${i}"}) {
       my $price_source = SL::PriceSource->new(record_item => $record_item, record => $record);
       my $price = $price_source->price_from_source($::form->{"active_price_source_$i"});
-      $::form->{price_sources}[$i] = $price_source;
-      $column_data{price_source} .= $cgi->button(-value => $price->full_description, -onClick => "toggle_price_source($i)");
+      $column_data{price_source} .= $cgi->button(-value => $price->full_description, -onClick => "kivi.io.price_chooser($i)");
+      if ($price->source) {
+        $column_data{price_source} .= ' ' . $cgi->img({src => 'image/flag-red.png', alt => $price->invalid, title => $price->invalid }) if $price->invalid;
+        $column_data{price_source} .= ' ' . $cgi->img({src => 'image/flag-red.png', alt => $price->missing, title => $price->missing }) if $price->missing;
+        $column_data{price_source} .= ' ' . $cgi->img({src => 'image/up.png',   alt => t8('This price has since gone up'),      title => t8('This price has since gone up' )     }) if $price->price > $record_item->sellprice;
+        $column_data{price_source} .= ' ' . $cgi->img({src => 'image/down.png', alt => t8('This price has since gone down'),    title => t8('This price has since gone down')    }) if $price->price < $record_item->sellprice;
+        $column_data{price_source} .= ' ' . $cgi->img({src => 'image/ok.png',   alt => t8('There is a better price available'), title => t8('There is a better price available') }) if $price->source ne $price_source->best_price->source;
+      }
     }
 
     if ($is_delivery_order) {
@@ -702,7 +709,8 @@ sub remove_emptied_rows {
                 price_old price_new unit_old ordnumber donumber
                 transdate longdescription basefactor marge_total marge_percent
                 marge_price_factor lastcost price_factor_id partnotes
-                stock_out stock_in has_sernumber reqdate orderitems_id);
+                stock_out stock_in has_sernumber reqdate orderitems_id
+                active_price_source);
 
   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
   push @flds, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
@@ -1931,8 +1939,6 @@ sub _make_record_item {
     next unless $obj->meta->column($method);
     if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
       $obj->${\"$method\_as_date"}($::form->{"$method\_$row"});
-    } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-      $obj->${\"$method\_as\_number"}($::form->{$method});
     } else {
       $obj->$method($::form->{"$method\_$row"});
     }
@@ -1978,7 +1984,7 @@ sub _make_record {
     if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
       $obj->${\"$method\_as_date"}($::form->{$method});
     } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-      $obj->${\"$method\_as\_number"}($::form->{$method});
+      $obj->${\"$method\_as_number"}($::form->{$method});
     } else {
       $obj->$method($::form->{$method});
     }
index 1ea357e..bf752b7 100644 (file)
@@ -335,7 +335,7 @@ sub form_header {
   ), @custom_hiddens,
   map { $_.'_rate', $_.'_description', $_.'_taxnumber' } split / /, $form->{taxaccounts}];
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
 
   $form->header();
 
index fa97118..66f6b44 100644 (file)
@@ -385,7 +385,7 @@ sub form_header {
   ), @custom_hiddens,
   map { $_.'_rate', $_.'_description', $_.'_taxnumber' } split / /, $form->{taxaccounts}];
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
 
   $form->header();
 
index 1460630..cc67e74 100644 (file)
@@ -465,7 +465,7 @@ sub form_header {
     }
   }
 
-  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase show_form_details show_history show_vc_details ckeditor/ckeditor ckeditor/adapters/jquery));
+  $::request->{layout}->use_javascript(map { "${_}.js" } qw(kivi.SalesPurchase show_form_details show_history show_vc_details ckeditor/ckeditor ckeditor/adapters/jquery kivi.io));
 
   $form->header;
   if ($form->{CFDD_shipto} && $form->{CFDD_shipto_id} ) {
diff --git a/js/kivi.io.js b/js/kivi.io.js
new file mode 100644 (file)
index 0000000..109b200
--- /dev/null
@@ -0,0 +1,46 @@
+namespace('kivi.io', function(ns) {
+  var $dialog;
+
+  ns.price_chooser_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.price_chooser = function(i) {
+    var form = $('form').serializeArray();
+    form.push( { name: 'action', value: 'PriceSource/price_popup' }
+             , { name: 'row',    value: i }
+    );
+
+    $.post('controller.pl', form, function(data) {
+      kivi.eval_json_result(data);
+    });
+  }
+
+  ns.update_price_source = function(row, source, price_str) {
+    $('#active_price_source_' + row).val(source);
+    if (price_str) $('#sellprice_' + row).val(price_str);
+    $('#update_button').click();
+  }
+});
index 0d0c635..dd175a9 100755 (executable)
@@ -291,6 +291,7 @@ $self->{texts} = {
   'Automatic deletion of leading, trailing and excessive (repetitive) spaces in part description and part notes. Affects the CSV import as well.' => 'Automatisches Löschen von voran-/nachgestellten und aufeinanderfolgenden Leerzeichen in Artikelbeschreibungen und -bemerkungen. Betrifft auch den CSV-Import.',
   'Automatically created invoice for fee and interest for dunning %s' => 'Automatisch erzeugte Rechnung für Gebühren und Zinsen zu Mahnung %s',
   'Available'                   => 'Verfügbar',
+  'Available Prices'            => 'Mögliche Preise',
   'Available qty'               => 'Lagerbestand',
   'BALANCE SHEET'               => 'BILANZ',
   'BIC'                         => 'BIC',
@@ -340,6 +341,7 @@ $self->{texts} = {
   'Beratername'                 => 'Beratername',
   'Beraternummer'               => 'Beraternummer',
   'Best Before'                 => 'Mindesthaltbarkeit',
+  'Best Price'                  => 'Bester Preis',
   'Bilanz'                      => 'Bilanz',
   'Billable amount'             => 'Abrechenbarer Betrag',
   'Billed amount'               => 'Abgerechneter Betrag',
@@ -2628,6 +2630,7 @@ $self->{texts} = {
   'There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?' => 'Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?',
   'There are undefined currencies in your system.' => 'In Ihrer Datenbank wurden Währungen benutzt, die nicht ordnungsgemäß in den Währungen eingetragen wurden.',
   'There are usually three ways to install Perl modules.' => 'Es gibt normalerweise drei Arten, ein Perlmodul zu installieren.',
+  'There is a better price available' => 'Es ist ein besserer Preis verfügbar',
   'There is already a taxkey 0 with tax rate not 0.' => 'Es existiert bereits ein Steuerschlüssel mit Steuersatz ungleich 0%.',
   'There is an inconsistancy in your database.' => 'In Ihrer Datenbank sind Unstimmigkeiten vorhanden.',
   'There is at least one sales or purchase invoice for which kivitendo recorded an inventory transaction with taxkeys even though no tax was recorded.' => 'Es gibt mindestens eine Verkaufs- oder Einkaufsrechnung, für die kivitendo eine Warenbestandsbuchung ohne dazugehörige Steuerbuchung durchgeführt hat.',
@@ -2663,6 +2666,8 @@ $self->{texts} = {
   'This option controls the method used for profit determination.' => 'Dieser Parameter legt die Berechnungsmethode für die Gewinnermittlung fest.',
   'This option controls the posting and calculation behavior for the accounting method.' => 'Dieser Parameter steuert die Buchungs- und Berechnungsmethoden für die Versteuerungsart.',
   'This partnumber is not unique. You should change it.' => 'Diese Artikelnummer ist nicht eindeutig. Bitte wählen Sie eine andere.',
+  'This price has since gone down' => 'Dieser Preis ist mittlerweile niedriger',
+  'This price has since gone up' => 'Dieser Preis ist mittlerweile höher',
   'This requirement spec is currently linked to the following project:' => 'Dieses Pflichtenheft ist mit dem folgenden Projekt verknüpft:',
   'This requirement spec is currently not linked to a project.' => 'Dieses Pflichtenheft ist noch nicht mit einem Projekt verknüpft.',
   'This requires you to manually correct entries for which an automatic conversion failed and to check those for which it succeeded.' => 'Dies erfordert, dass Sie diejenigen Einträge manuell korrigieren, für die die automatische Umstellung fehlschlug, sowie dass Sie diejenigen überprüfen, für die die Umstellung erfolgreich war.',
@@ -2770,6 +2775,7 @@ $self->{texts} = {
   'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)',
   'Until'                       => 'Bis',
   'Update'                      => 'Erneuern',
+  'Update Price'                => 'Preis übernehmen',
   'Update Prices'               => 'Preise aktualisieren',
   'Update SKR04: new tax account 3804 (19%)' => 'Update SKR04: neues Steuerkonto 3804 (19%) für innergemeinschaftlichen Erwerb',
   'Update prices'               => 'Preise aktualisieren',
diff --git a/templates/webpages/oe/_price_sources_row.html b/templates/webpages/oe/_price_sources_row.html
deleted file mode 100644 (file)
index a58e139..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-[%- USE T8 %]
-[%- USE HTML %]
-[%- USE L %]
-[%- USE LxERP %]
-<tr class="listrow[% i % 2 %]" id="row[% i %]_3" style='display:none'>
- <td colspan="[% row.colspan %]">
-   <span class="[% IF !row.obj.active_price_source %]bold[% END %]">
-   [% L.radio_button_tag('active_price_source_' _ i, label=LxERP.t8('None (PriceSource)'), checked=!row.obj.active_price_source, value='', onChange='update_price_source(' _ i _ ', \'\')') %]
-   </span>
-   [%- FOREACH price IN price_sources.$i.available_prices %]
-     <div class="[% IF price.source == row.obj.active_price_source %]bold[% END %]">
-     [% L.radio_button_tag('active_price_source_' _ i, value=price.source, checked=price.source == row.obj.active_price_source, label=LxERP.format_amount(price.price, 2) _ ' (' _ price.full_description _ ')', onChange='update_price_source(' _ i _ ', \'' _ price.source _ '\', \'' _ LxERP.format_amount(price.price, -2) _ '\')' ) %]
-     </div>
-   [%- END %]
- </td>
-</tr>
diff --git a/templates/webpages/oe/price_sources_dialog.html b/templates/webpages/oe/price_sources_dialog.html
new file mode 100644 (file)
index 0000000..92ee4eb
--- /dev/null
@@ -0,0 +1,41 @@
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[% SET best_price = price_source.best_price %]
+  <table>
+   <tr class='listheading'>
+    <th></th>
+    <th>[% 'Price Source' | $T8 %]</th>
+    <th>[% 'Price' | $T8 %]</th>
+    <th>[% 'Best Price' | $T8 %]</th>
+   </tr>
+   <tr class='listrow'>
+[%- IF price_source.record_item.active_price_source %]
+    <td>[% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'\')', LxERP.t8('Select')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[%- END %]
+    <td>[% 'None (PriceSource)' | $T8 %]</td>
+    <td>-</td>
+    <td></td>
+   </tr>
+   [%- FOREACH price IN price_source.available_prices %]
+    <tr class='listrow'>
+[%- IF price_source.record_item.active_price_source != price.source %]
+     <td>[% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'' _ price.source _ '\', \'' _ LxERP.format_amount(price.price, -2) _ '\')', LxERP.t8('Select')) %]</td>
+[%- ELSIF price_source.record_item.sellprice_as_number != price.price_as_number %]
+     <td>[% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'' _ price.source _ '\', \'' _ LxERP.format_amount(price.price, -2) _ '\')', LxERP.t8('Update Price')) %]</td>
+[%- ELSE %]
+    <td><b>[% 'Selected' | $T8 %]</b></td>
+[% END %]
+     <td>[% price.full_description | html %]</td>
+     <td>[% price.price_as_number %]</td>
+[% IF price.source == best_price.source %]
+     <td align='center'>&#x2022;</td>
+[% ELSE %]
+     <td></td>
+[% END %]
+    </tr>
+   [%- END %]
+  </table>
index 91f0a56..12be937 100644 (file)
@@ -75,7 +75,6 @@
 
       </td>
      </tr>
- [% PROCESS 'oe/_price_sources_row.html' i = loop.count %]
 [%- END %]
 
   </table>
         [% END %]
       }, 1);
     });
-    function toggle_price_source(row) {
-      $('#row' + row + '_3').toggle();
-    }
-    function update_price_source(row, source, price_str){
-      $('#active_price_source_' + row).val(source);
-      if (price_str) $('#sellprice_' + row).val(price_str);
-      $('#update_button').click();
-    }
   </script>
 
  </td>