Order Controller - lastcost von Positionen mit speichern
[kivitendo-erp.git] / SL / Controller / Order.pm
index 6e88036..8023aa2 100644 (file)
@@ -3,36 +3,26 @@ package SL::Controller::Order;
 use strict;
 use parent qw(SL::Controller::Base);
 
-use SL::Helper::Flash;
+use SL::Helper::Flash qw(flash_later);
 use SL::Presenter;
-use SL::Locale::String;
+use SL::Locale::String qw(t8);
 use SL::SessionFile::Random;
 use SL::PriceSource;
-use SL::Form;
 use SL::Webdav;
-use SL::Template;
 
 use SL::DB::Order;
-use SL::DB::Customer;
-use SL::DB::Vendor;
-use SL::DB::TaxZone;
-use SL::DB::Employee;
-use SL::DB::Project;
 use SL::DB::Default;
 use SL::DB::Unit;
-use SL::DB::Price;
-use SL::DB::PriceFactor;
 use SL::DB::Part;
 use SL::DB::Printer;
 use SL::DB::Language;
 
-use SL::Helper::DateTime;
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 
 use SL::Controller::Helper::GetModels;
 
-use List::Util qw(max first);
+use List::Util qw(first);
 use List::MoreUtils qw(none pairwise first_index);
 use English qw(-no_match_vars);
 use File::Spec;
@@ -48,7 +38,7 @@ use Rose::Object::MakeMethods::Generic
 __PACKAGE__->run_before('_check_auth');
 
 __PACKAGE__->run_before('_recalc',
-                        only => [ qw(update save save_and_delivery_order print create_pdf send_email) ]);
+                        only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
 
 __PACKAGE__->run_before('_get_unalterable_data',
                         only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
@@ -57,6 +47,7 @@ __PACKAGE__->run_before('_get_unalterable_data',
 # actions
 #
 
+# add a new order
 sub action_add {
   my ($self) = @_;
 
@@ -73,6 +64,7 @@ sub action_add {
   );
 }
 
+# edit an existing order
 sub action_edit {
   my ($self) = @_;
 
@@ -88,19 +80,7 @@ sub action_edit {
   );
 }
 
-sub action_update {
-  my ($self) = @_;
-
-  $self->_pre_render();
-  $self->render(
-    'order/form',
-    title => $self->type eq _sales_order_type()    ? $::locale->text('Edit Sales Order')
-           : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
-           : '',
-    %{$self->{template_args}}
-  );
-}
-
+# delete the order
 sub action_delete {
   my ($self) = @_;
 
@@ -120,6 +100,7 @@ sub action_delete {
   $self->redirect_to(@redirect_params);
 }
 
+# save the order
 sub action_save {
   my ($self) = @_;
 
@@ -140,6 +121,14 @@ sub action_save {
   $self->redirect_to(@redirect_params);
 }
 
+# print the order
+#
+# This is called if "print" is pressed in the print dialog.
+# If PDF creation was requested and succeeded, the pdf is stored in a session
+# file and the filename is stored as session value with an unique key. A
+# javascript function with this key is then called. This function calls the
+# download action below (action_download_pdf), which offers the file for
+# download.
 sub action_print {
   my ($self) = @_;
 
@@ -189,26 +178,16 @@ sub action_print {
     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
 
     $self->js
-    ->run('download_pdf', $pdf_filename, $key)
+    ->run('kivi.Order.download_pdf', $pdf_filename, $key)
     ->flash('info', t8('The PDF has been created'));
 
   } elsif ($media eq 'printer') {
     # printer
     my $printer_id = $::form->{print_options}->{printer_id};
-    my $printer;
-    $printer = SL::DB::Printer->new(id => $printer_id)->load if $printer_id;
-    if (!$printer) {
-      return $self->js->flash('error', t8('Printer not found.'))->render;
-    }
-
-    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
-
-    for my $i (1 .. $copies) {
-      open my $out, '|-', $command or die $!;
-      binmode $out;
-      print $out $pdf;
-      close $out;
-    }
+    SL::DB::Printer->new(id => $printer_id)->load->print_document(
+      copies  => $copies,
+      content => $pdf,
+    );
 
     $self->js->flash('info', t8('The PDF has been printed'));
   }
@@ -234,6 +213,9 @@ sub action_print {
   $self->js->render;
 }
 
+# offer pdf for download
+#
+# It needs to get the key for the session value to get the pdf file.
 sub action_download_pdf {
   my ($self) = @_;
 
@@ -246,6 +228,7 @@ sub action_download_pdf {
   );
 }
 
+# open the email dialog
 sub action_show_email_dialog {
   my ($self) = @_;
 
@@ -275,11 +258,13 @@ sub action_show_email_dialog {
 
   my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
   $self->js
-      ->run('show_email_dialog', $dialog_html)
+      ->run('kivi.Order.show_email_dialog', $dialog_html)
       ->reinit_widgets
       ->render($self);
 }
 
+# send email
+#
 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
 sub action_send_email {
   my ($self) = @_;
@@ -315,10 +300,12 @@ sub action_send_email {
 
   $self->js
       ->val('#order_intnotes', $intnotes)
-      ->run('close_email_dialog')
+      ->run('kivi.Order.close_email_dialog')
       ->render($self);
 }
 
+# save the order and redirect to the frontend subroutine for a new
+# delivery order
 sub action_save_and_delivery_order {
   my ($self) = @_;
 
@@ -339,6 +326,9 @@ sub action_save_and_delivery_order {
   $self->redirect_to(@redirect_params);
 }
 
+# set form elements in respect of a changed customer or vendor
+#
+# This action is called on an change of the customer/vendor picker.
 sub action_customer_vendor_changed {
   my ($self) = @_;
 
@@ -383,6 +373,7 @@ sub action_customer_vendor_changed {
   $self->js->render();
 }
 
+# called if a unit in an existing item row is changed
 sub action_unit_changed {
   my ($self) = @_;
 
@@ -395,12 +386,13 @@ sub action_unit_changed {
   $self->_recalc();
 
   $self->js
-    ->run('update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
+    ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
   $self->_js_redisplay_linetotals;
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
+# add an item row for a new item entered in the input row
 sub action_add_item {
   my ($self) = @_;
 
@@ -423,24 +415,23 @@ sub action_add_item {
   $self->js
     ->append('#row_table_id', $row_as_html)
     ->val('.add_item_input', '')
-    ->run('row_table_scroll_down')
-    ->run('row_set_keyboard_events_by_id', $item_id)
-    ->run('set_unit_change_with_oldval_by_id', $item_id)
-    ->run('renumber_positions')
-    ->on('.recalc', 'change', 'recalc_amounts_and_taxes')
-    ->on('.reformat_number', 'change', 'reformat_number')
+    ->run('kivi.Order.init_row_handlers')
+    ->run('kivi.Order.row_table_scroll_down')
+    ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
+# open the dialog for entering multiple items at once
 sub action_show_multi_items_dialog {
   require SL::DB::PartsGroup;
   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
 }
 
+# update the filter results in the multi item dialog
 sub action_multi_items_update_result {
   my $max_count = 100;
 
@@ -461,6 +452,7 @@ sub action_multi_items_update_result {
   }
 }
 
+# add item rows for multiple items add once
 sub action_add_multi_items {
   my ($self) = @_;
 
@@ -483,24 +475,21 @@ sub action_add_multi_items {
                                        ALL_PRICE_FACTORS => $self->all_price_factors
     );
 
-    $self->js
-        ->append('#row_table_id', $row_as_html)
-        ->run('row_set_keyboard_events_by_id', $item_id)
-        ->run('set_unit_change_with_oldval_by_id', $item_id);
+    $self->js->append('#row_table_id', $row_as_html);
   }
 
   $self->js
-    ->run('close_multi_items_dialog')
-    ->run('row_table_scroll_down')
-    ->run('renumber_positions')
-    ->on('.recalc', 'change', 'recalc_amounts_and_taxes')
-    ->on('.reformat_number', 'change', 'reformat_number')
+    ->run('kivi.Order.close_multi_items_dialog')
+    ->run('kivi.Order.init_row_handlers')
+    ->run('kivi.Order.row_table_scroll_down')
+    ->run('kivi.Order.renumber_positions')
     ->focus('#add_item_parts_id_name');
 
   $self->_js_redisplay_amounts_and_taxes;
   $self->js->render();
 }
 
+# recalculate all linetotals, amounts and taxes and redisplay them
 sub action_recalc_amounts_and_taxes {
   my ($self) = @_;
 
@@ -511,6 +500,7 @@ sub action_recalc_amounts_and_taxes {
   $self->js->render();
 }
 
+# redisplay item rows if the are sorted by an attribute
 sub action_reorder_items {
   my ($self) = @_;
 
@@ -530,10 +520,11 @@ sub action_reorder_items {
     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
   }
   $self->js
-    ->run('redisplay_items', \@to_sort)
+    ->run('kivi.Order.redisplay_items', \@to_sort)
     ->render;
 }
 
+# show the popup to choose a price/discount source
 sub action_price_popup {
   my ($self) = @_;
 
@@ -543,6 +534,11 @@ sub action_price_popup {
   $self->render_price_dialog($item);
 }
 
+# get the longdescription for an item if the dialog to enter/change the
+# longdescription was opened and the longdescription is empty
+#
+# If this item is new, get the longdescription from Part.
+# Get it from OrderItem else.
 sub action_get_item_longdescription {
   my $longdescription;
 
@@ -554,13 +550,12 @@ sub action_get_item_longdescription {
   $_[0]->render(\ $longdescription, { type => 'text' });
 }
 
-
 sub _js_redisplay_linetotals {
   my ($self) = @_;
 
   my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items_sorted };
   $self->js
-    ->run('redisplay_linetotals', \@data);
+    ->run('kivi.Order.redisplay_linetotals', \@data);
 }
 
 sub _js_redisplay_amounts_and_taxes {
@@ -621,6 +616,7 @@ sub init_order {
   $_[0]->_make_order;
 }
 
+# model used to filter/display the parts in the multi-items dialog
 sub init_multi_items_models {
   SL::Controller::Helper::GetModels->new(
     controller     => $_[0],
@@ -653,6 +649,9 @@ sub _check_auth {
   $::auth->assert($right);
 }
 
+# build the selection box for contacts
+#
+# Needed, if customer/vendor changed.
 sub build_contact_select {
   my ($self) = @_;
 
@@ -665,6 +664,9 @@ sub build_contact_select {
   );
 }
 
+# build the selection box for shiptos
+#
+# Needed, if customer/vendor changed.
 sub build_shipto_select {
   my ($self) = @_;
 
@@ -677,6 +679,9 @@ sub build_shipto_select {
   );
 }
 
+# build the rows for displaying taxes
+#
+# Called if amounts where recalculated and redisplayed.
 sub build_tax_rows {
   my ($self) = @_;
 
@@ -717,6 +722,12 @@ sub _load_order {
   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
 }
 
+# load or create a new order object
+#
+# And assign changes from the for to this object.
+# If the order is loaded from db, check if items are deleted in the form,
+# remove them form the object and collect them for removing from db on saving.
+# Then create/update items from form (via _make_item) and add them.
 sub _make_order {
   my ($self) = @_;
 
@@ -753,7 +764,8 @@ sub _make_order {
   return $order;
 }
 
-
+# create or update items from form
+#
 # Make item objects from form values. For items already existing read from db.
 # Create a new item else. And assign attributes.
 sub _make_item {
@@ -771,10 +783,15 @@ sub _make_item {
 
   $item->assign_attributes(%$attr);
   $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
+  # item fields that currently can't be set in in row but are needed:
+  $item->lastcost($item->part->lastcost);
 
   return $item;
 }
 
+# create a new item
+#
+# This is used to add one (or more) items
 sub _new_item {
   my ($record, $attr) = @_;
 
@@ -830,6 +847,9 @@ sub _new_item {
   return $item;
 }
 
+# recalculate prices and taxes
+#
+# Using the PriceTaxCalculator. Store linetotals in the item objects.
 sub _recalc {
   my ($self) = @_;
 
@@ -850,7 +870,9 @@ sub _recalc {
   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
 }
 
-
+# get data for saving, printing, ..., that is not changed in the form
+#
+# Only cvars for now.
 sub _get_unalterable_data {
   my ($self) = @_;
 
@@ -864,7 +886,9 @@ sub _get_unalterable_data {
   }
 }
 
-
+# delete the order
+#
+# And remove related files in the spool directory
 sub _delete {
   my ($self) = @_;
 
@@ -884,7 +908,9 @@ sub _delete {
   return $errors;
 }
 
-
+# save the order
+#
+# And delete items that are deleted in the form.
 sub _save {
   my ($self) = @_;
 
@@ -953,7 +979,7 @@ sub _pre_render {
                                                 } } @all_objects;
   }
 
-  $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase ckeditor/ckeditor ckeditor/adapters/jquery);
+  $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
 }
 
 sub _create_pdf {
@@ -972,8 +998,6 @@ sub _create_pdf {
   $print_form->{language_id} = $params->{language}->id            if $print_form->{language};
 
   $order->flatten_to_form($print_form, format_amounts => 1);
-  # flatten_to_form sets payment_terms from customer/vendor - we do not want that here
-  delete $print_form->{payment_terms} if !$print_form->{payment_id};
 
   # search for the template
   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
@@ -1018,3 +1042,219 @@ sub _purchase_order_type {
 }
 
 1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::Controller::Order - controller for orders
+
+=head1 SYNOPSIS
+
+This is a new form to enter orders, completely rewritten with the use
+of controller and java script techniques.
+
+The aim is to provide the user a better expirience and a faster flow
+of work. Also the code should be more readable, more reliable and
+better to maintain.
+
+=head2 Key Features
+
+=over 4
+
+=item *
+
+One input row, so that input happens every time at the same place.
+
+=item *
+
+Use of pickers where possible.
+
+=item *
+
+Possibility to enter more than one item at once.
+
+=item *
+
+Save order only on "save" (and "save and delivery order"-workflow). No
+hidden save on "print" or "email".
+
+=item *
+
+Item list in a scrollable area, so that the workflow buttons stay at
+the bottom.
+
+=item *
+
+Reordering item rows with drag and drop is possible. Sorting item rows is
+possible (by partnumber, description, qty, sellprice and discount for now).
+
+=item *
+
+No C<update> is necessary. All entries and calculations are managed
+with ajax-calls and the page does only reload on C<save>.
+
+=item *
+
+User can see changes immediately, because of the use of java script
+and ajax.
+
+=back
+
+=head1 CODE
+
+=head2 Layout
+
+=over 4
+
+=item * C<SL/Controller/Order.pm>
+
+the controller
+
+=item * C<template/webpages/order/form.html>
+
+main form
+
+=item * C<template/webpages/order/tabs/basic_data.html>
+
+Main tab for basic_data.
+
+This is the only tab here for now. "linked records" and "webdav" tabs are
+reused from generic code.
+
+=over 4
+
+=item * C<template/webpages/order/tabs/_item_input.html>
+
+The input line for items
+
+=item * C<template/webpages/order/tabs/_row.html>
+
+One row for already entered items
+
+=item * C<template/webpages/order/tabs/_tax_row.html>
+
+Displaying tax information
+
+=item * C<template/webpages/order/tabs/_multi_items_dialog.html>
+
+Dialog for entering more than one item at once
+
+=item * C<template/webpages/order/tabs/_multi_items_result.html>
+
+Results for the filter in the multi items dialog
+
+=item * C<template/webpages/order/tabs/_price_sources_dialog.html>
+
+Dialog for selecting price and discount sources
+
+=item * C<template/webpages/order/tabs/_email_dialog.html>
+
+Email dialog
+
+=back
+
+=item * C<js/kivi.Order.js>
+
+java script functions
+
+=back
+
+=head1 TODO
+
+=over 4
+
+=item * testing
+
+=item * currency
+
+=item * customer/vendor details ('D'-button)
+
+=item * credit limit
+
+=item * more workflows (save as new / invoice)
+
+=item * price sources: little symbols showing better price / better discount
+
+=item * custom shipto address
+
+=item * periodic invoices
+
+=item * more details on second row (marge, ...)
+
+=item * language / part translations
+
+=item * access rights
+
+=item * preset salesman from customer
+
+=item * display weights
+
+=item * force project if enabled in client config
+
+=item * history
+
+=item * mtime check
+
+=back
+
+=head1 KNOWN BUGS AND CAVEATS
+
+=over 4
+
+=item *
+
+Customer discount is not displayed as a valid discount in price source popup
+(this might be a bug in price sources)
+
+=item *
+
+No indication that double click expands second row, no exand all button
+
+=item *
+
+Implementation of second row with a tbody for every item is not supported by
+our css.
+
+=item *
+
+As a consequence row striping does not currently work
+
+=item *
+
+Inline creation of parts is not currently supported
+
+=item *
+
+Table header is not sticky in the scrolling area.
+
+=item *
+
+Sorting does not include C<position>, neither does reordering.
+
+This behavior was implemented intentionally. But we can discuss, which behavior
+should be implemented.
+
+=item *
+
+C<show_multi_items_dialog> does not use the currently inserted string for
+filtering.
+
+=item * Performance
+
+Rendering a 50 items order takes twice as long as the old code.
+
+90% of that is rendering the (hidden) second rows, and 50% of those again are
+checks for is_valid and C<INCLUDE> on the cvar input template.
+
+Suggestion: fetch second rows when asked for.
+
+=back
+
+=head1 AUTHOR
+
+Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
+
+=cut