Auftrags-Controller: Beim Drucken Sprache richtig berücksichtigen.
[kivitendo-erp.git] / SL / Controller / Order.pm
1 package SL::Controller::Order;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Presenter::Tag qw(select_tag);
8 use SL::Locale::String qw(t8);
9 use SL::SessionFile::Random;
10 use SL::PriceSource;
11 use SL::Webdav;
12 use SL::File;
13
14 use SL::DB::Order;
15 use SL::DB::Default;
16 use SL::DB::Unit;
17 use SL::DB::Part;
18 use SL::DB::Printer;
19 use SL::DB::Language;
20
21 use SL::Helper::CreatePDF qw(:all);
22 use SL::Helper::PrintOptions;
23
24 use SL::Controller::Helper::GetModels;
25
26 use List::Util qw(first);
27 use List::MoreUtils qw(none pairwise first_index);
28 use English qw(-no_match_vars);
29 use File::Spec;
30
31 use Rose::Object::MakeMethods::Generic
32 (
33  scalar => [ qw(item_ids_to_delete) ],
34  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
35 );
36
37
38 # safety
39 __PACKAGE__->run_before('_check_auth');
40
41 __PACKAGE__->run_before('_recalc',
42                         only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
43
44 __PACKAGE__->run_before('_get_unalterable_data',
45                         only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
46
47 #
48 # actions
49 #
50
51 # add a new order
52 sub action_add {
53   my ($self) = @_;
54
55   $self->order->transdate(DateTime->now_local());
56   $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
57
58   $self->_pre_render();
59   $self->render(
60     'order/form',
61     title => $self->type eq _sales_order_type()    ? $::locale->text('Add Sales Order')
62            : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
63            : '',
64     %{$self->{template_args}}
65   );
66 }
67
68 # edit an existing order
69 sub action_edit {
70   my ($self) = @_;
71
72   $self->_load_order;
73   $self->_recalc();
74   $self->_pre_render();
75   $self->render(
76     'order/form',
77     title => $self->type eq _sales_order_type()    ? $::locale->text('Edit Sales Order')
78            : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
79            : '',
80     %{$self->{template_args}}
81   );
82 }
83
84 # delete the order
85 sub action_delete {
86   my ($self) = @_;
87
88   my $errors = $self->_delete();
89
90   if (scalar @{ $errors }) {
91     $self->js->flash('error', $_) foreach @{ $errors };
92     return $self->js->render();
93   }
94
95   flash_later('info', $::locale->text('The order has been deleted'));
96   my @redirect_params = (
97     action => 'add',
98     type   => $self->type,
99   );
100
101   $self->redirect_to(@redirect_params);
102 }
103
104 # save the order
105 sub action_save {
106   my ($self) = @_;
107
108   my $errors = $self->_save();
109
110   if (scalar @{ $errors }) {
111     $self->js->flash('error', $_) foreach @{ $errors };
112     return $self->js->render();
113   }
114
115   flash_later('info', $::locale->text('The order has been saved'));
116   my @redirect_params = (
117     action => 'edit',
118     type   => $self->type,
119     id     => $self->order->id,
120   );
121
122   $self->redirect_to(@redirect_params);
123 }
124
125 # print the order
126 #
127 # This is called if "print" is pressed in the print dialog.
128 # If PDF creation was requested and succeeded, the pdf is stored in a session
129 # file and the filename is stored as session value with an unique key. A
130 # javascript function with this key is then called. This function calls the
131 # download action below (action_download_pdf), which offers the file for
132 # download.
133 sub action_print {
134   my ($self) = @_;
135
136   my $format      = $::form->{print_options}->{format};
137   my $media       = $::form->{print_options}->{media};
138   my $formname    = $::form->{print_options}->{formname};
139   my $copies      = $::form->{print_options}->{copies};
140   my $groupitems  = $::form->{print_options}->{groupitems};
141
142   # only pdf by now
143   if (none { $format eq $_ } qw(pdf)) {
144     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
145   }
146
147   # only screen or printer by now
148   if (none { $media eq $_ } qw(screen printer)) {
149     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
150   }
151
152   my $language;
153   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
154
155   # create a form for generate_attachment_filename
156   my $form = Form->new;
157   $form->{ordnumber} = $self->order->ordnumber;
158   $form->{type}      = $self->type;
159   $form->{format}    = $format;
160   $form->{formname}  = $formname;
161   $form->{language}  = '_' . $language->template_code if $language;
162   my $pdf_filename   = $form->generate_attachment_filename();
163
164   my $pdf;
165   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
166                                                   formname   => $formname,
167                                                   language   => $language,
168                                                   groupitems => $groupitems });
169   if (scalar @errors) {
170     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
171   }
172
173   if ($media eq 'screen') {
174     # screen/download
175     my $sfile = SL::SessionFile::Random->new(mode => "w");
176     $sfile->fh->print($pdf);
177     $sfile->fh->close;
178
179     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
180     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
181
182     $self->js
183     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
184     ->flash('info', t8('The PDF has been created'));
185
186   } elsif ($media eq 'printer') {
187     # printer
188     my $printer_id = $::form->{print_options}->{printer_id};
189     SL::DB::Printer->new(id => $printer_id)->load->print_document(
190       copies  => $copies,
191       content => $pdf,
192     );
193
194     $self->js->flash('info', t8('The PDF has been printed'));
195   }
196
197   # copy file to webdav folder
198   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
199     my $webdav = SL::Webdav->new(
200       type     => $self->type,
201       number   => $self->order->ordnumber,
202     );
203     my $webdav_file = SL::Webdav::File->new(
204       webdav   => $webdav,
205       filename => $pdf_filename,
206     );
207     eval {
208       $webdav_file->store(data => \$pdf);
209       1;
210     } or do {
211       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
212     }
213   }
214   if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
215     eval {
216       SL::File->save(object_id     => $self->order->id,
217                      object_type   => $self->type,
218                      mime_type     => 'application/pdf',
219                      source        => 'created',
220                      file_type     => 'document',
221                      file_name     => $pdf_filename,
222                      file_contents => $pdf);
223       1;
224     } or do {
225       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
226     }
227   }
228   $self->js->render;
229 }
230
231 # offer pdf for download
232 #
233 # It needs to get the key for the session value to get the pdf file.
234 sub action_download_pdf {
235   my ($self) = @_;
236
237   my $key = $::form->{key};
238   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
239   return $self->send_file(
240     $tmp_filename,
241     type => 'application/pdf',
242     name => $::form->{pdf_filename},
243   );
244 }
245
246 # open the email dialog
247 sub action_show_email_dialog {
248   my ($self) = @_;
249
250   my $cv_method = $self->cv;
251
252   if (!$self->order->$cv_method) {
253     return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
254                     ->render($self);
255   }
256
257   $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
258   $self->{email}->{to} ||= $self->order->$cv_method->email;
259   $self->{email}->{cc}   = $self->order->$cv_method->cc;
260   $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
261   # Todo: get addresses from shipto, if any
262
263   my $form = Form->new;
264   $form->{ordnumber} = $self->order->ordnumber;
265   $form->{formname}  = $self->type;
266   $form->{type}      = $self->type;
267   $form->{language} = 'de';
268   $form->{format}   = 'pdf';
269
270   $self->{email}->{subject}             = $form->generate_email_subject();
271   $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
272   $self->{email}->{message}             = $form->create_email_signature();
273
274   my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
275   $self->js
276       ->run('kivi.Order.show_email_dialog', $dialog_html)
277       ->reinit_widgets
278       ->render($self);
279 }
280
281 # send email
282 #
283 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
284 sub action_send_email {
285   my ($self) = @_;
286
287   my $mail      = Mailer->new;
288   $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
289   $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
290
291   my $pdf;
292   my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
293   if (scalar @errors) {
294     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
295   }
296
297   $mail->{attachments} = [{ "content" => $pdf,
298                             "name"    => $::form->{email}->{attachment_filename} }];
299
300   if (my $err = $mail->send) {
301     return $self->js->flash('error', t8('Sending E-mail: ') . $err)
302                     ->render($self);
303   }
304
305   # internal notes
306   my $intnotes = $self->order->intnotes;
307   $intnotes   .= "\n\n" if $self->order->intnotes;
308   $intnotes   .= t8('[email]')                                                                                        . "\n";
309   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
310   $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
311   $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
312   $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
313   $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
314   $intnotes   .= t8('Message')    . ": " . $mail->{message};
315
316   $self->js
317       ->val('#order_intnotes', $intnotes)
318       ->run('kivi.Order.close_email_dialog')
319       ->render($self);
320 }
321
322 # save the order and redirect to the frontend subroutine for a new
323 # delivery order
324 sub action_save_and_delivery_order {
325   my ($self) = @_;
326
327   my $errors = $self->_save();
328
329   if (scalar @{ $errors }) {
330     $self->js->flash('error', $_) foreach @{ $errors };
331     return $self->js->render();
332   }
333   flash_later('info', $::locale->text('The order has been saved'));
334
335   my @redirect_params = (
336     controller => 'oe.pl',
337     action     => 'oe_delivery_order_from_order',
338     id         => $self->order->id,
339   );
340
341   $self->redirect_to(@redirect_params);
342 }
343
344 # save the order and redirect to the frontend subroutine for a new
345 # invoice
346 sub action_save_and_invoice {
347   my ($self) = @_;
348
349   my $errors = $self->_save();
350
351   if (scalar @{ $errors }) {
352     $self->js->flash('error', $_) foreach @{ $errors };
353     return $self->js->render();
354   }
355   flash_later('info', $::locale->text('The order has been saved'));
356
357   my @redirect_params = (
358     controller => 'oe.pl',
359     action     => 'oe_invoice_from_order',
360     id         => $self->order->id,
361   );
362
363   $self->redirect_to(@redirect_params);
364 }
365
366 # set form elements in respect to a changed customer or vendor
367 #
368 # This action is called on an change of the customer/vendor picker.
369 sub action_customer_vendor_changed {
370   my ($self) = @_;
371
372   my $cv_method = $self->cv;
373
374   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
375     $self->js->show('#cp_row');
376   } else {
377     $self->js->hide('#cp_row');
378   }
379
380   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
381     $self->js->show('#shipto_row');
382   } else {
383     $self->js->hide('#shipto_row');
384   }
385
386   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
387
388   if ($self->order->is_sales) {
389     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
390                               ? $self->order->$cv_method->taxincluded_checked
391                               : $::myconfig{taxincluded_checked});
392     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
393   }
394
395   $self->order->payment_id($self->order->$cv_method->payment_id);
396   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
397
398   $self->_recalc();
399
400   $self->js
401     ->replaceWith('#order_cp_id',            $self->build_contact_select)
402     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
403     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
404     ->val(        '#order_taxincluded',      $self->order->taxincluded)
405     ->val(        '#order_payment_id',       $self->order->payment_id)
406     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
407     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
408     ->focus(      '#order_' . $self->cv . '_id');
409
410   $self->_js_redisplay_amounts_and_taxes;
411   $self->js->render();
412 }
413
414 # called if a unit in an existing item row is changed
415 sub action_unit_changed {
416   my ($self) = @_;
417
418   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
419   my $item = $self->order->items_sorted->[$idx];
420
421   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
422   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
423
424   $self->_recalc();
425
426   $self->js
427     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
428   $self->_js_redisplay_line_values;
429   $self->_js_redisplay_amounts_and_taxes;
430   $self->js->render();
431 }
432
433 # add an item row for a new item entered in the input row
434 sub action_add_item {
435   my ($self) = @_;
436
437   my $form_attr = $::form->{add_item};
438
439   return unless $form_attr->{parts_id};
440
441   my $item = _new_item($self->order, $form_attr);
442
443   $self->order->add_items($item);
444
445   $self->_recalc();
446
447   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
448   my $row_as_html = $self->p->render('order/tabs/_row',
449                                      ITEM              => $item,
450                                      ID                => $item_id,
451                                      TYPE              => $self->type,
452                                      ALL_PRICE_FACTORS => $self->all_price_factors
453   );
454
455   $self->js
456     ->append('#row_table_id', $row_as_html);
457
458   if ( $item->part->is_assortment ) {
459     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
460     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
461       my $attr = { parts_id => $assortment_item->parts_id,
462                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
463                    unit     => $assortment_item->unit,
464                    description => $assortment_item->part->description,
465                  };
466       my $item = _new_item($self->order, $attr);
467
468       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
469       $item->discount(1) unless $assortment_item->charge;
470
471       $self->order->add_items( $item );
472       $self->_recalc();
473       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
474       my $row_as_html = $self->p->render('order/tabs/_row',
475                                          ITEM              => $item,
476                                          ID                => $item_id,
477                                          TYPE              => $self->type,
478                                          ALL_PRICE_FACTORS => $self->all_price_factors
479       );
480       $self->js
481         ->append('#row_table_id', $row_as_html);
482     };
483   };
484
485   $self->js
486     ->val('.add_item_input', '')
487     ->run('kivi.Order.init_row_handlers')
488     ->run('kivi.Order.row_table_scroll_down')
489     ->run('kivi.Order.renumber_positions')
490     ->focus('#add_item_parts_id_name');
491
492   $self->_js_redisplay_amounts_and_taxes;
493   $self->js->render();
494 }
495
496 # open the dialog for entering multiple items at once
497 sub action_show_multi_items_dialog {
498   require SL::DB::PartsGroup;
499   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
500                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
501 }
502
503 # update the filter results in the multi item dialog
504 sub action_multi_items_update_result {
505   my $max_count = 100;
506
507   $::form->{multi_items}->{filter}->{obsolete} = 0;
508
509   my $count = $_[0]->multi_items_models->count;
510
511   if ($count == 0) {
512     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
513     $_[0]->render($text, { layout => 0 });
514   } elsif ($count > $max_count) {
515     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
516     $_[0]->render($text, { layout => 0 });
517   } else {
518     my $multi_items = $_[0]->multi_items_models->get;
519     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
520                   multi_items => $multi_items);
521   }
522 }
523
524 # add item rows for multiple items at once
525 sub action_add_multi_items {
526   my ($self) = @_;
527
528   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
529   return $self->js->render() unless scalar @form_attr;
530
531   my @items;
532   foreach my $attr (@form_attr) {
533     my $item = _new_item($self->order, $attr);
534     push @items, $item;
535     if ( $item->part->is_assortment ) {
536       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
537         my $attr = { parts_id => $assortment_item->parts_id,
538                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
539                      unit     => $assortment_item->unit,
540                      description => $assortment_item->part->description,
541                    };
542         my $item = _new_item($self->order, $attr);
543
544         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
545         $item->discount(1) unless $assortment_item->charge;
546         push @items, $item;
547       }
548     }
549   }
550   $self->order->add_items(@items);
551
552   $self->_recalc();
553
554   foreach my $item (@items) {
555     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
556     my $row_as_html = $self->p->render('order/tabs/_row',
557                                        ITEM              => $item,
558                                        ID                => $item_id,
559                                        TYPE              => $self->type,
560                                        ALL_PRICE_FACTORS => $self->all_price_factors
561     );
562
563     $self->js->append('#row_table_id', $row_as_html);
564   }
565
566   $self->js
567     ->run('kivi.Order.close_multi_items_dialog')
568     ->run('kivi.Order.init_row_handlers')
569     ->run('kivi.Order.row_table_scroll_down')
570     ->run('kivi.Order.renumber_positions')
571     ->focus('#add_item_parts_id_name');
572
573   $self->_js_redisplay_amounts_and_taxes;
574   $self->js->render();
575 }
576
577 # recalculate all linetotals, amounts and taxes and redisplay them
578 sub action_recalc_amounts_and_taxes {
579   my ($self) = @_;
580
581   $self->_recalc();
582
583   $self->_js_redisplay_line_values;
584   $self->_js_redisplay_amounts_and_taxes;
585   $self->js->render();
586 }
587
588 # redisplay item rows if they are sorted by an attribute
589 sub action_reorder_items {
590   my ($self) = @_;
591
592   my %sort_keys = (
593     partnumber  => sub { $_[0]->part->partnumber },
594     description => sub { $_[0]->description },
595     qty         => sub { $_[0]->qty },
596     sellprice   => sub { $_[0]->sellprice },
597     discount    => sub { $_[0]->discount },
598   );
599
600   my $method = $sort_keys{$::form->{order_by}};
601   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
602   if ($::form->{sort_dir}) {
603     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
604   } else {
605     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
606   }
607   $self->js
608     ->run('kivi.Order.redisplay_items', \@to_sort)
609     ->render;
610 }
611
612 # show the popup to choose a price/discount source
613 sub action_price_popup {
614   my ($self) = @_;
615
616   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
617   my $item = $self->order->items_sorted->[$idx];
618
619   $self->render_price_dialog($item);
620 }
621
622 # get the longdescription for an item if the dialog to enter/change the
623 # longdescription was opened and the longdescription is empty
624 #
625 # If this item is new, get the longdescription from Part.
626 # Otherwise get it from OrderItem.
627 sub action_get_item_longdescription {
628   my $longdescription;
629
630   if ($::form->{item_id}) {
631     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
632   } elsif ($::form->{parts_id}) {
633     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
634   }
635   $_[0]->render(\ $longdescription, { type => 'text' });
636 }
637
638 # load the second row for one or more items
639 #
640 # This action gets the html code for all items second rows by rendering a template for
641 # the second row and sets the html code via client js.
642 sub action_load_second_rows {
643   my ($self) = @_;
644
645   $self->_recalc() if $self->order->is_sales; # for margin calculation
646
647   foreach my $item_id (@{ $::form->{item_ids} }) {
648     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
649     my $item = $self->order->items_sorted->[$idx];
650
651     $self->_js_load_second_row($item, $item_id, 0);
652   }
653
654   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
655
656   $self->js->render();
657 }
658
659 sub _js_load_second_row {
660   my ($self, $item, $item_id, $do_parse) = @_;
661
662   if ($do_parse) {
663     # Parse values from form (they are formated while rendering (template)).
664     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
665     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
666     foreach my $var (@{ $item->cvars_by_config }) {
667       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
668     }
669     $item->parse_custom_variable_values;
670   }
671
672   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
673
674   $self->js
675     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
676     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
677 }
678
679 sub _js_redisplay_line_values {
680   my ($self) = @_;
681
682   my $is_sales = $self->order->is_sales;
683
684   # sales orders with margins
685   my @data;
686   if ($is_sales) {
687     @data = map {
688       [
689        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
690        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
691        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
692       ]} @{ $self->order->items_sorted };
693   } else {
694     @data = map {
695       [
696        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
697       ]} @{ $self->order->items_sorted };
698   }
699
700   $self->js
701     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
702 }
703
704 sub _js_redisplay_amounts_and_taxes {
705   my ($self) = @_;
706
707   if (scalar @{ $self->{taxes} }) {
708     $self->js->show('#taxincluded_row_id');
709   } else {
710     $self->js->hide('#taxincluded_row_id');
711   }
712
713   if ($self->order->taxincluded) {
714     $self->js->hide('#subtotal_row_id');
715   } else {
716     $self->js->show('#subtotal_row_id');
717   }
718
719   $self->js
720     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
721     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
722     ->remove('.tax_row')
723     ->insertBefore($self->build_tax_rows, '#amount_row_id');
724 }
725
726 #
727 # helpers
728 #
729
730 sub init_valid_types {
731   [ _sales_order_type(), _purchase_order_type() ];
732 }
733
734 sub init_type {
735   my ($self) = @_;
736
737   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
738     die "Not a valid type for order";
739   }
740
741   $self->type($::form->{type});
742 }
743
744 sub init_cv {
745   my ($self) = @_;
746
747   my $cv = $self->type eq _sales_order_type()    ? 'customer'
748          : $self->type eq _purchase_order_type() ? 'vendor'
749          : die "Not a valid type for order";
750
751   return $cv;
752 }
753
754 sub init_p {
755   SL::Presenter->get;
756 }
757
758 sub init_order {
759   $_[0]->_make_order;
760 }
761
762 # model used to filter/display the parts in the multi-items dialog
763 sub init_multi_items_models {
764   SL::Controller::Helper::GetModels->new(
765     controller     => $_[0],
766     model          => 'Part',
767     with_objects   => [ qw(unit_obj) ],
768     disable_plugin => 'paginated',
769     source         => $::form->{multi_items},
770     sorted         => {
771       _default    => {
772         by  => 'partnumber',
773         dir => 1,
774       },
775       partnumber  => t8('Partnumber'),
776       description => t8('Description')}
777   );
778 }
779
780 sub init_all_price_factors {
781   SL::DB::Manager::PriceFactor->get_all;
782 }
783
784 sub _check_auth {
785   my ($self) = @_;
786
787   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
788
789   my $right   = $right_for->{ $self->type };
790   $right    ||= 'DOES_NOT_EXIST';
791
792   $::auth->assert($right);
793 }
794
795 # build the selection box for contacts
796 #
797 # Needed, if customer/vendor changed.
798 sub build_contact_select {
799   my ($self) = @_;
800
801   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
802     value_key  => 'cp_id',
803     title_key  => 'full_name_dep',
804     default    => $self->order->cp_id,
805     with_empty => 1,
806     style      => 'width: 300px',
807   );
808 }
809
810 # build the selection box for shiptos
811 #
812 # Needed, if customer/vendor changed.
813 sub build_shipto_select {
814   my ($self) = @_;
815
816   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
817     value_key  => 'shipto_id',
818     title_key  => 'displayable_id',
819     default    => $self->order->shipto_id,
820     with_empty => 1,
821     style      => 'width: 300px',
822   );
823 }
824
825 # build the rows for displaying taxes
826 #
827 # Called if amounts where recalculated and redisplayed.
828 sub build_tax_rows {
829   my ($self) = @_;
830
831   my $rows_as_html;
832   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
833     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
834   }
835   return $rows_as_html;
836 }
837
838
839 sub render_price_dialog {
840   my ($self, $record_item) = @_;
841
842   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
843
844   $self->js
845     ->run(
846       'kivi.io.price_chooser_dialog',
847       t8('Available Prices'),
848       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
849     )
850     ->reinit_widgets;
851
852 #   if (@errors) {
853 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
854 #     $self->js->show('#dialog_flash_error');
855 #   }
856
857   $self->js->render;
858 }
859
860 sub _load_order {
861   my ($self) = @_;
862
863   return if !$::form->{id};
864
865   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
866 }
867
868 # load or create a new order object
869 #
870 # And assign changes from the for to this object.
871 # If the order is loaded from db, check if items are deleted in the form,
872 # remove them form the object and collect them for removing from db on saving.
873 # Then create/update items from form (via _make_item) and add them.
874 sub _make_order {
875   my ($self) = @_;
876
877   # add_items adds items to an order with no items for saving, but they cannot
878   # be retrieved via items until the order is saved. Adding empty items to new
879   # order here solves this problem.
880   my $order;
881   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
882   $order ||= SL::DB::Order->new(orderitems => []);
883
884   my $form_orderitems = delete $::form->{order}->{orderitems};
885   $order->assign_attributes(%{$::form->{order}});
886
887   # remove deleted items
888   $self->item_ids_to_delete([]);
889   foreach my $idx (reverse 0..$#{$order->orderitems}) {
890     my $item = $order->orderitems->[$idx];
891     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
892       splice @{$order->orderitems}, $idx, 1;
893       push @{$self->item_ids_to_delete}, $item->id;
894     }
895   }
896
897   my @items;
898   my $pos = 1;
899   foreach my $form_attr (@{$form_orderitems}) {
900     my $item = _make_item($order, $form_attr);
901     $item->position($pos);
902     push @items, $item;
903     $pos++;
904   }
905   $order->add_items(grep {!$_->id} @items);
906
907   return $order;
908 }
909
910 # create or update items from form
911 #
912 # Make item objects from form values. For items already existing read from db.
913 # Create a new item else. And assign attributes.
914 sub _make_item {
915   my ($record, $attr) = @_;
916
917   my $item;
918   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
919
920   my $is_new = !$item;
921
922   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
923   # they cannot be retrieved via custom_variables until the order/orderitem is
924   # saved. Adding empty custom_variables to new orderitem here solves this problem.
925   $item ||= SL::DB::OrderItem->new(custom_variables => []);
926
927   $item->assign_attributes(%$attr);
928   $item->longdescription($item->part->notes)   if $is_new && !defined $attr->{longdescription};
929   $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
930   $item->lastcost($item->part->lastcost)       if $is_new && !defined $attr->{lastcost_as_number};
931
932   return $item;
933 }
934
935 # create a new item
936 #
937 # This is used to add one item
938 sub _new_item {
939   my ($record, $attr) = @_;
940
941   my $item = SL::DB::OrderItem->new;
942   $item->assign_attributes(%$attr);
943
944   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
945   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
946
947   $item->unit($part->unit) if !$item->unit;
948
949   my $price_src;
950   if ( $part->is_assortment ) {
951     # add assortment items with price 0, as the components carry the price
952     $price_src = $price_source->price_from_source("");
953     $price_src->price(0);
954   } elsif ($item->sellprice) {
955     $price_src = $price_source->price_from_source("");
956     $price_src->price($item->sellprice);
957   } else {
958     $price_src = $price_source->best_price
959            ? $price_source->best_price
960            : $price_source->price_from_source("");
961     $price_src->price(0) if !$price_source->best_price;
962   }
963
964   my $discount_src;
965   if ($item->discount) {
966     $discount_src = $price_source->discount_from_source("");
967     $discount_src->discount($item->discount);
968   } else {
969     $discount_src = $price_source->best_discount
970                   ? $price_source->best_discount
971                   : $price_source->discount_from_source("");
972     $discount_src->discount(0) if !$price_source->best_discount;
973   }
974
975   my %new_attr;
976   $new_attr{part}                   = $part;
977   $new_attr{description}            = $part->description     if ! $item->description;
978   $new_attr{qty}                    = 1.0                    if ! $item->qty;
979   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
980   $new_attr{sellprice}              = $price_src->price;
981   $new_attr{discount}               = $discount_src->discount;
982   $new_attr{active_price_source}    = $price_src;
983   $new_attr{active_discount_source} = $discount_src;
984   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
985   $new_attr{project_id}             = $record->globalproject_id;
986   $new_attr{lastcost}               = $part->lastcost;
987
988   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
989   # they cannot be retrieved via custom_variables until the order/orderitem is
990   # saved. Adding empty custom_variables to new orderitem here solves this problem.
991   $new_attr{custom_variables} = [];
992
993   $item->assign_attributes(%new_attr);
994
995   return $item;
996 }
997
998 # recalculate prices and taxes
999 #
1000 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1001 sub _recalc {
1002   my ($self) = @_;
1003
1004   # bb: todo: currency later
1005   $self->order->currency_id($::instance_conf->get_currency_id());
1006
1007   my %pat = $self->order->calculate_prices_and_taxes();
1008   $self->{taxes} = [];
1009   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1010     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1011
1012     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1013     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
1014                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1015                                 tax       => $tax });
1016   }
1017
1018   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1019 }
1020
1021 # get data for saving, printing, ..., that is not changed in the form
1022 #
1023 # Only cvars for now.
1024 sub _get_unalterable_data {
1025   my ($self) = @_;
1026
1027   foreach my $item (@{ $self->order->items }) {
1028     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1029     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1030     foreach my $var (@{ $item->cvars_by_config }) {
1031       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1032     }
1033     $item->parse_custom_variable_values;
1034   }
1035 }
1036
1037 # delete the order
1038 #
1039 # And remove related files in the spool directory
1040 sub _delete {
1041   my ($self) = @_;
1042
1043   my $errors = [];
1044   my $db     = $self->order->db;
1045
1046   $db->with_transaction(
1047     sub {
1048       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1049       $self->order->delete;
1050       my $spool = $::lx_office_conf{paths}->{spool};
1051       unlink map { "$spool/$_" } @spoolfiles if $spool;
1052
1053       1;
1054   }) || push(@{$errors}, $db->error);
1055
1056   return $errors;
1057 }
1058
1059 # save the order
1060 #
1061 # And delete items that are deleted in the form.
1062 sub _save {
1063   my ($self) = @_;
1064
1065   my $errors = [];
1066   my $db     = $self->order->db;
1067
1068   $db->with_transaction(sub {
1069     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1070     $self->order->save(cascade => 1);
1071   }) || push(@{$errors}, $db->error);
1072
1073   return $errors;
1074 }
1075
1076
1077 sub _pre_render {
1078   my ($self) = @_;
1079
1080   $self->{all_taxzones}        = SL::DB::Manager::TaxZone->get_all_sorted();
1081   $self->{all_departments}     = SL::DB::Manager::Department->get_all_sorted();
1082   $self->{all_employees}       = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1083                                                                                        deleted => 0 ] ],
1084                                                                     sort_by => 'name');
1085   $self->{all_salesmen}        = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1086                                                                                        deleted => 0 ] ],
1087                                                                     sort_by => 'name');
1088   $self->{all_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1089                                                                                       active => 1 ] ],
1090                                                                    sort_by => 'projectnumber');
1091   $self->{all_payment_terms}   = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1092                                                                                                  obsolete => 0 ] ]);
1093
1094   $self->{all_delivery_terms}  = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1095
1096   $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1097
1098   my $print_form = Form->new('');
1099   $print_form->{type}      = $self->type;
1100   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
1101   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1102   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
1103     form => $print_form,
1104     options => {dialog_name_prefix => 'print_options.',
1105                 show_headers       => 1,
1106                 no_queue           => 1,
1107                 no_postscript      => 1,
1108                 no_opendocument    => 1,
1109                 no_html            => 1},
1110   );
1111
1112   foreach my $item (@{$self->order->orderitems}) {
1113     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1114     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1115     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1116   }
1117
1118   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1119     my $webdav = SL::Webdav->new(
1120       type     => $self->type,
1121       number   => $self->order->ordnumber,
1122     );
1123     my @all_objects = $webdav->get_all_objects;
1124     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1125                                                     type => t8('File'),
1126                                                     link => File::Spec->catfile($_->full_filedescriptor),
1127                                                 } } @all_objects;
1128   }
1129
1130   $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery);
1131   $self->_setup_edit_action_bar;
1132 }
1133
1134 sub _setup_edit_action_bar {
1135   my ($self, %params) = @_;
1136
1137   my $deletion_allowed = (($self->cv eq 'customer') && $::instance_conf->get_sales_order_show_delete)
1138                       || (($self->cv eq 'vendor')   && $::instance_conf->get_purchase_order_show_delete);
1139
1140   for my $bar ($::request->layout->get('actionbar')) {
1141     $bar->add(
1142       combobox => [
1143         action => [
1144           t8('Save'),
1145           call      => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1146           accesskey => 'enter',
1147         ],
1148         action => [
1149           t8('Save and Delivery Order'),
1150           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1151         ],
1152         action => [
1153           t8('Save and Invoice'),
1154           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1155         ],
1156
1157       ], # end of combobox "Save"
1158
1159       combobox => [
1160         action => [
1161           t8('Export'),
1162         ],
1163         action => [
1164           t8('Print'),
1165           call => [ 'kivi.Order.show_print_options' ],
1166         ],
1167         action => [
1168           t8('E-mail'),
1169           call => [ 'kivi.Order.email' ],
1170         ],
1171         action => [
1172           t8('Download attachments of all parts'),
1173           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1174           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1175           only_if  => $::instance_conf->get_doc_storage,
1176         ],
1177       ], # end of combobox "Export"
1178
1179       action => [
1180         t8('Delete'),
1181         call     => [ 'kivi.Order.delete_order' ],
1182         confirm  => $::locale->text('Do you really want to delete this object?'),
1183         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1184         only_if  => $deletion_allowed,
1185       ],
1186     );
1187   }
1188 }
1189
1190 sub _create_pdf {
1191   my ($order, $pdf_ref, $params) = @_;
1192
1193   my @errors = ();
1194
1195   my $print_form = Form->new('');
1196   $print_form->{type}        = $order->type;
1197   $print_form->{formname}    = $params->{formname} || $order->type;
1198   $print_form->{format}      = $params->{format}   || 'pdf';
1199   $print_form->{media}       = $params->{media}    || 'file';
1200   $print_form->{groupitems}  = $params->{groupitems};
1201   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1202
1203   $order->language($params->{language});
1204   $order->flatten_to_form($print_form, format_amounts => 1);
1205
1206   # search for the template
1207   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1208     name        => $print_form->{formname},
1209     email       => $print_form->{media} eq 'email',
1210     language    => $params->{language},
1211     printer_id  => $print_form->{printer_id},  # todo
1212   );
1213
1214   if (!defined $template_file) {
1215     push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
1216   }
1217
1218   return @errors if scalar @errors;
1219
1220   $print_form->throw_on_error(sub {
1221     eval {
1222       $print_form->prepare_for_printing;
1223
1224       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1225         template  => $template_file,
1226         variables => $print_form,
1227         variable_content_types => {
1228           longdescription => 'html',
1229           partnotes       => 'html',
1230           notes           => 'html',
1231         },
1232       );
1233       1;
1234     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1235   });
1236
1237   return @errors;
1238 }
1239
1240 sub _sales_order_type {
1241   'sales_order';
1242 }
1243
1244 sub _purchase_order_type {
1245   'purchase_order';
1246 }
1247
1248 1;
1249
1250 __END__
1251
1252 =encoding utf-8
1253
1254 =head1 NAME
1255
1256 SL::Controller::Order - controller for orders
1257
1258 =head1 SYNOPSIS
1259
1260 This is a new form to enter orders, completely rewritten with the use
1261 of controller and java script techniques.
1262
1263 The aim is to provide the user a better expirience and a faster flow
1264 of work. Also the code should be more readable, more reliable and
1265 better to maintain.
1266
1267 =head2 Key Features
1268
1269 =over 4
1270
1271 =item *
1272
1273 One input row, so that input happens every time at the same place.
1274
1275 =item *
1276
1277 Use of pickers where possible.
1278
1279 =item *
1280
1281 Possibility to enter more than one item at once.
1282
1283 =item *
1284
1285 Save order only on "save" (and "save and delivery order"-workflow). No
1286 hidden save on "print" or "email".
1287
1288 =item *
1289
1290 Item list in a scrollable area, so that the workflow buttons stay at
1291 the bottom.
1292
1293 =item *
1294
1295 Reordering item rows with drag and drop is possible. Sorting item rows is
1296 possible (by partnumber, description, qty, sellprice and discount for now).
1297
1298 =item *
1299
1300 No C<update> is necessary. All entries and calculations are managed
1301 with ajax-calls and the page does only reload on C<save>.
1302
1303 =item *
1304
1305 User can see changes immediately, because of the use of java script
1306 and ajax.
1307
1308 =back
1309
1310 =head1 CODE
1311
1312 =head2 Layout
1313
1314 =over 4
1315
1316 =item * C<SL/Controller/Order.pm>
1317
1318 the controller
1319
1320 =item * C<template/webpages/order/form.html>
1321
1322 main form
1323
1324 =item * C<template/webpages/order/tabs/basic_data.html>
1325
1326 Main tab for basic_data.
1327
1328 This is the only tab here for now. "linked records" and "webdav" tabs are
1329 reused from generic code.
1330
1331 =over 4
1332
1333 =item * C<template/webpages/order/tabs/_item_input.html>
1334
1335 The input line for items
1336
1337 =item * C<template/webpages/order/tabs/_row.html>
1338
1339 One row for already entered items
1340
1341 =item * C<template/webpages/order/tabs/_tax_row.html>
1342
1343 Displaying tax information
1344
1345 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1346
1347 Dialog for entering more than one item at once
1348
1349 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1350
1351 Results for the filter in the multi items dialog
1352
1353 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1354
1355 Dialog for selecting price and discount sources
1356
1357 =item * C<template/webpages/order/tabs/_email_dialog.html>
1358
1359 Email dialog
1360
1361 =back
1362
1363 =item * C<js/kivi.Order.js>
1364
1365 java script functions
1366
1367 =back
1368
1369 =head1 TODO
1370
1371 =over 4
1372
1373 =item * testing
1374
1375 =item * currency
1376
1377 =item * customer/vendor details ('D'-button)
1378
1379 =item * credit limit
1380
1381 =item * more workflows (save as new, quotation, purchase order)
1382
1383 =item * price sources: little symbols showing better price / better discount
1384
1385 =item * select units in input row?
1386
1387 =item * custom shipto address
1388
1389 =item * periodic invoices
1390
1391 =item * language / part translations
1392
1393 =item * access rights
1394
1395 =item * display weights
1396
1397 =item * history
1398
1399 =item * mtime check
1400
1401 =item * optional client/user behaviour
1402
1403 (transactions has to be set - department has to be set -
1404  force project if enabled in client config - transport cost reminder)
1405
1406 =back
1407
1408 =head1 KNOWN BUGS AND CAVEATS
1409
1410 =over 4
1411
1412 =item *
1413
1414 Customer discount is not displayed as a valid discount in price source popup
1415 (this might be a bug in price sources)
1416
1417 (I cannot reproduce this (Bernd))
1418
1419 =item *
1420
1421 No indication that <shift>-up/down expands/collapses second row.
1422
1423 =item *
1424
1425 Inline creation of parts is not currently supported
1426
1427 =item *
1428
1429 Table header is not sticky in the scrolling area.
1430
1431 =item *
1432
1433 Sorting does not include C<position>, neither does reordering.
1434
1435 This behavior was implemented intentionally. But we can discuss, which behavior
1436 should be implemented.
1437
1438 =item *
1439
1440 C<show_multi_items_dialog> does not use the currently inserted string for
1441 filtering.
1442
1443 =back
1444
1445 =head1 To discuss / Nice to have
1446
1447 =over 4
1448
1449 =item *
1450
1451 How to expand/collapse second row. Now it can be done clicking the icon or
1452 <shift>-up/down.
1453
1454 =item *
1455
1456 Possibility to change longdescription in input row?
1457
1458 =item *
1459
1460 Possibility to select PriceSources in input row?
1461
1462 =item *
1463
1464 This controller uses a (changed) copy of the template for the PriceSource
1465 dialog. Maybe there could be used one code source.
1466
1467 =item *
1468
1469 Rounding-differences between this controller (PriceTaxCalculator) and the old
1470 form. This is not only a problem here, but also in all parts using the PTC.
1471 There exists a ticket and a patch. This patch should be testet.
1472
1473 =item *
1474
1475 An indicator, if the actual inputs are saved (like in an
1476 editor or on text processing application).
1477
1478 =item *
1479
1480 A warning when leaving the page without saveing unchanged inputs.
1481
1482 =item *
1483
1484 Workflows for delivery order and invoice are in the menu "Save", because the
1485 order is saved before opening the new document form. Nevertheless perhaps these
1486 workflow buttons should be put under "Workflows".
1487
1488
1489 =back
1490
1491 =head1 AUTHOR
1492
1493 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1494
1495 =cut