From 049e49fea3e4a3c7c78d7aebf055936b3cab40c5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sven=20Sch=C3=B6ling?= Date: Tue, 29 Jul 2014 17:36:40 +0200 Subject: [PATCH] PriceSource: Preisselektion auf Popup umgestellt. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 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 --- SL/Controller/PriceSource.pm | 167 ++++++++++++++++++ SL/PriceSource.pm | 4 +- SL/PriceSource/Base.pm | 2 + bin/mozilla/do.pl | 2 +- bin/mozilla/io.pl | 18 +- bin/mozilla/ir.pl | 2 +- bin/mozilla/is.pl | 2 +- bin/mozilla/oe.pl | 2 +- js/kivi.io.js | 46 +++++ locale/de/all | 6 + templates/webpages/oe/_price_sources_row.html | 16 -- .../webpages/oe/price_sources_dialog.html | 41 +++++ templates/webpages/oe/sales_order.html | 9 - 13 files changed, 280 insertions(+), 37 deletions(-) create mode 100644 SL/Controller/PriceSource.pm create mode 100644 js/kivi.io.js delete mode 100644 templates/webpages/oe/_price_sources_row.html create mode 100644 templates/webpages/oe/price_sources_dialog.html diff --git a/SL/Controller/PriceSource.pm b/SL/Controller/PriceSource.pm new file mode 100644 index 000000000..baed7d40b --- /dev/null +++ b/SL/Controller/PriceSource.pm @@ -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; + diff --git a/SL/PriceSource.pm b/SL/PriceSource.pm index 470182cc9..5237003fe 100644 --- a/SL/PriceSource.pm +++ b/SL/PriceSource.pm @@ -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 diff --git a/SL/PriceSource/Base.pm b/SL/PriceSource/Base.pm index b095684cf..261897d25 100644 --- a/SL/PriceSource/Base.pm +++ b/SL/PriceSource/Base.pm @@ -183,6 +183,8 @@ The price field in purchase records is still C. C and C are tainted. If you store data directly in C, sanitize. +=back + =head1 SEE ALSO L, diff --git a/bin/mozilla/do.pl b/bin/mozilla/do.pl index 1bda974ab..b242acb67 100644 --- a/bin/mozilla/do.pl +++ b/bin/mozilla/do.pl @@ -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' diff --git a/bin/mozilla/io.pl b/bin/mozilla/io.pl index c7a06af28..ecf7723e6 100644 --- a/bin/mozilla/io.pl +++ b/bin/mozilla/io.pl @@ -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}); } diff --git a/bin/mozilla/ir.pl b/bin/mozilla/ir.pl index 1ea357ec4..bf752b764 100644 --- a/bin/mozilla/ir.pl +++ b/bin/mozilla/ir.pl @@ -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(); diff --git a/bin/mozilla/is.pl b/bin/mozilla/is.pl index fa97118d9..66f6b44c5 100644 --- a/bin/mozilla/is.pl +++ b/bin/mozilla/is.pl @@ -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(); diff --git a/bin/mozilla/oe.pl b/bin/mozilla/oe.pl index 146063080..cc67e746f 100644 --- a/bin/mozilla/oe.pl +++ b/bin/mozilla/oe.pl @@ -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 index 000000000..109b20010 --- /dev/null +++ b/js/kivi.io.js @@ -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 = $('').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(); + } +}); diff --git a/locale/de/all b/locale/de/all index 0d0c63566..dd175a9c7 100755 --- a/locale/de/all +++ b/locale/de/all @@ -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 index a58e13973..000000000 --- a/templates/webpages/oe/_price_sources_row.html +++ /dev/null @@ -1,16 +0,0 @@ -[%- USE T8 %] -[%- USE HTML %] -[%- USE L %] -[%- USE LxERP %] - - - - [% 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 _ ', \'\')') %] - - [%- FOREACH price IN price_sources.$i.available_prices %] -
- [% 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) _ '\')' ) %] -
- [%- END %] - - diff --git a/templates/webpages/oe/price_sources_dialog.html b/templates/webpages/oe/price_sources_dialog.html new file mode 100644 index 000000000..92ee4ebfe --- /dev/null +++ b/templates/webpages/oe/price_sources_dialog.html @@ -0,0 +1,41 @@ +[%- USE T8 %] +[%- USE HTML %] +[%- USE L %] +[%- USE LxERP %] +[% SET best_price = price_source.best_price %] + + + + + + + + +[%- IF price_source.record_item.active_price_source %] + +[%- ELSE %] + +[%- END %] + + + + + [%- FOREACH price IN price_source.available_prices %] + +[%- IF price_source.record_item.active_price_source != price.source %] + +[%- ELSIF price_source.record_item.sellprice_as_number != price.price_as_number %] + +[%- ELSE %] + +[% END %] + + +[% IF price.source == best_price.source %] + +[% ELSE %] + +[% END %] + + [%- END %] +
[% 'Price Source' | $T8 %][% 'Price' | $T8 %][% 'Best Price' | $T8 %]
[% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'\')', LxERP.t8('Select')) %][% 'Selected' | $T8 %][% 'None (PriceSource)' | $T8 %]-
[% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'' _ price.source _ '\', \'' _ LxERP.format_amount(price.price, -2) _ '\')', LxERP.t8('Select')) %][% L.button_tag('kivi.io.update_price_source(' _ FORM.row _ ', \'' _ price.source _ '\', \'' _ LxERP.format_amount(price.price, -2) _ '\')', LxERP.t8('Update Price')) %][% 'Selected' | $T8 %][% price.full_description | html %][% price.price_as_number %]
diff --git a/templates/webpages/oe/sales_order.html b/templates/webpages/oe/sales_order.html index 91f0a56c6..12be937e0 100644 --- a/templates/webpages/oe/sales_order.html +++ b/templates/webpages/oe/sales_order.html @@ -75,7 +75,6 @@ - [% PROCESS 'oe/_price_sources_row.html' i = loop.count %] [%- END %] @@ -95,14 +94,6 @@ [% 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(); - } -- 2.20.1