Auftrags-Controller: Fehler bei Anzeige von EK und Marge in 2. Zeile behoben
[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   my $form = Form->new;
156   $form->{ordnumber} = $self->order->ordnumber;
157   $form->{type}      = $self->type;
158   $form->{format}    = $format;
159   $form->{formname}  = $formname;
160   $form->{language}  = '_' . $language->template_code if $language;
161   my $pdf_filename   = $form->generate_attachment_filename();
162
163   my $pdf;
164   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
165                                                   formname   => $formname,
166                                                   language   => $language,
167                                                   groupitems => $groupitems });
168   if (scalar @errors) {
169     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
170   }
171
172   if ($media eq 'screen') {
173     # screen/download
174     my $sfile = SL::SessionFile::Random->new(mode => "w");
175     $sfile->fh->print($pdf);
176     $sfile->fh->close;
177
178     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
179     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
180
181     $self->js
182     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
183     ->flash('info', t8('The PDF has been created'));
184
185   } elsif ($media eq 'printer') {
186     # printer
187     my $printer_id = $::form->{print_options}->{printer_id};
188     SL::DB::Printer->new(id => $printer_id)->load->print_document(
189       copies  => $copies,
190       content => $pdf,
191     );
192
193     $self->js->flash('info', t8('The PDF has been printed'));
194   }
195
196   # copy file to webdav folder
197   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
198     my $webdav = SL::Webdav->new(
199       type     => $self->type,
200       number   => $self->order->ordnumber,
201     );
202     my $webdav_file = SL::Webdav::File->new(
203       webdav   => $webdav,
204       filename => $pdf_filename,
205     );
206     eval {
207       $webdav_file->store(data => \$pdf);
208       1;
209     } or do {
210       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
211     }
212   }
213   if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
214     eval {
215       SL::File->save(object_id     => $self->order->id,
216                      object_type   => $self->type,
217                      mime_type     => 'application/pdf',
218                      source        => 'created',
219                      file_type     => 'document',
220                      file_name     => $pdf_filename,
221                      file_contents => $pdf);
222       1;
223     } or do {
224       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
225     }
226   }
227   $self->js->render;
228 }
229
230 # offer pdf for download
231 #
232 # It needs to get the key for the session value to get the pdf file.
233 sub action_download_pdf {
234   my ($self) = @_;
235
236   my $key = $::form->{key};
237   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
238   return $self->send_file(
239     $tmp_filename,
240     type => 'application/pdf',
241     name => $::form->{pdf_filename},
242   );
243 }
244
245 # open the email dialog
246 sub action_show_email_dialog {
247   my ($self) = @_;
248
249   my $cv_method = $self->cv;
250
251   if (!$self->order->$cv_method) {
252     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'))
253                     ->render($self);
254   }
255
256   $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
257   $self->{email}->{to} ||= $self->order->$cv_method->email;
258   $self->{email}->{cc}   = $self->order->$cv_method->cc;
259   $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
260   # Todo: get addresses from shipto, if any
261
262   my $form = Form->new;
263   $form->{ordnumber} = $self->order->ordnumber;
264   $form->{formname}  = $self->type;
265   $form->{type}      = $self->type;
266   $form->{language} = 'de';
267   $form->{format}   = 'pdf';
268
269   $self->{email}->{subject}             = $form->generate_email_subject();
270   $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
271   $self->{email}->{message}             = $form->create_email_signature();
272
273   my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
274   $self->js
275       ->run('kivi.Order.show_email_dialog', $dialog_html)
276       ->reinit_widgets
277       ->render($self);
278 }
279
280 # send email
281 #
282 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
283 sub action_send_email {
284   my ($self) = @_;
285
286   my $mail      = Mailer->new;
287   $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
288   $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
289
290   my $pdf;
291   my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
292   if (scalar @errors) {
293     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
294   }
295
296   $mail->{attachments} = [{ "content" => $pdf,
297                             "name"    => $::form->{email}->{attachment_filename} }];
298
299   if (my $err = $mail->send) {
300     return $self->js->flash('error', t8('Sending E-mail: ') . $err)
301                     ->render($self);
302   }
303
304   # internal notes
305   my $intnotes = $self->order->intnotes;
306   $intnotes   .= "\n\n" if $self->order->intnotes;
307   $intnotes   .= t8('[email]')                                                                                        . "\n";
308   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
309   $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
310   $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
311   $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
312   $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
313   $intnotes   .= t8('Message')    . ": " . $mail->{message};
314
315   $self->js
316       ->val('#order_intnotes', $intnotes)
317       ->run('kivi.Order.close_email_dialog')
318       ->render($self);
319 }
320
321 # save the order and redirect to the frontend subroutine for a new
322 # delivery order
323 sub action_save_and_delivery_order {
324   my ($self) = @_;
325
326   my $errors = $self->_save();
327
328   if (scalar @{ $errors }) {
329     $self->js->flash('error', $_) foreach @{ $errors };
330     return $self->js->render();
331   }
332   flash_later('info', $::locale->text('The order has been saved'));
333
334   my @redirect_params = (
335     controller => 'oe.pl',
336     action     => 'oe_delivery_order_from_order',
337     id         => $self->order->id,
338   );
339
340   $self->redirect_to(@redirect_params);
341 }
342
343 # save the order and redirect to the frontend subroutine for a new
344 # invoice
345 sub action_save_and_invoice {
346   my ($self) = @_;
347
348   my $errors = $self->_save();
349
350   if (scalar @{ $errors }) {
351     $self->js->flash('error', $_) foreach @{ $errors };
352     return $self->js->render();
353   }
354   flash_later('info', $::locale->text('The order has been saved'));
355
356   my @redirect_params = (
357     controller => 'oe.pl',
358     action     => 'oe_invoice_from_order',
359     id         => $self->order->id,
360   );
361
362   $self->redirect_to(@redirect_params);
363 }
364
365 # set form elements in respect to a changed customer or vendor
366 #
367 # This action is called on an change of the customer/vendor picker.
368 sub action_customer_vendor_changed {
369   my ($self) = @_;
370
371   my $cv_method = $self->cv;
372
373   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
374     $self->js->show('#cp_row');
375   } else {
376     $self->js->hide('#cp_row');
377   }
378
379   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
380     $self->js->show('#shipto_row');
381   } else {
382     $self->js->hide('#shipto_row');
383   }
384
385   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
386
387   if ($self->order->is_sales) {
388     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
389                               ? $self->order->$cv_method->taxincluded_checked
390                               : $::myconfig{taxincluded_checked});
391     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
392   }
393
394   $self->order->payment_id($self->order->$cv_method->payment_id);
395   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
396
397   $self->_recalc();
398
399   $self->js
400     ->replaceWith('#order_cp_id',            $self->build_contact_select)
401     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
402     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
403     ->val(        '#order_taxincluded',      $self->order->taxincluded)
404     ->val(        '#order_payment_id',       $self->order->payment_id)
405     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
406     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
407     ->focus(      '#order_' . $self->cv . '_id');
408
409   $self->_js_redisplay_amounts_and_taxes;
410   $self->js->render();
411 }
412
413 # called if a unit in an existing item row is changed
414 sub action_unit_changed {
415   my ($self) = @_;
416
417   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
418   my $item = $self->order->items_sorted->[$idx];
419
420   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
421   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
422
423   $self->_recalc();
424
425   $self->js
426     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
427   $self->_js_redisplay_line_values;
428   $self->_js_redisplay_amounts_and_taxes;
429   $self->js->render();
430 }
431
432 # add an item row for a new item entered in the input row
433 sub action_add_item {
434   my ($self) = @_;
435
436   my $form_attr = $::form->{add_item};
437
438   return unless $form_attr->{parts_id};
439
440   my $item = _new_item($self->order, $form_attr);
441
442   $self->order->add_items($item);
443
444   $self->_recalc();
445
446   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
447   my $row_as_html = $self->p->render('order/tabs/_row',
448                                      ITEM              => $item,
449                                      ID                => $item_id,
450                                      TYPE              => $self->type,
451                                      ALL_PRICE_FACTORS => $self->all_price_factors
452   );
453
454   $self->js
455     ->append('#row_table_id', $row_as_html);
456
457   if ( $item->part->is_assortment ) {
458     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
459     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
460       my $attr = { parts_id => $assortment_item->parts_id,
461                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
462                    unit     => $assortment_item->unit,
463                    description => $assortment_item->part->description,
464                  };
465       my $item = _new_item($self->order, $attr);
466
467       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
468       $item->discount(1) unless $assortment_item->charge;
469
470       $self->order->add_items( $item );
471       $self->_recalc();
472       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
473       my $row_as_html = $self->p->render('order/tabs/_row',
474                                          ITEM              => $item,
475                                          ID                => $item_id,
476                                          TYPE              => $self->type,
477                                          ALL_PRICE_FACTORS => $self->all_price_factors
478       );
479       $self->js
480         ->append('#row_table_id', $row_as_html);
481     };
482   };
483
484   $self->js
485     ->val('.add_item_input', '')
486     ->run('kivi.Order.init_row_handlers')
487     ->run('kivi.Order.row_table_scroll_down')
488     ->run('kivi.Order.renumber_positions')
489     ->focus('#add_item_parts_id_name');
490
491   $self->_js_redisplay_amounts_and_taxes;
492   $self->js->render();
493 }
494
495 # open the dialog for entering multiple items at once
496 sub action_show_multi_items_dialog {
497   require SL::DB::PartsGroup;
498   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
499                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
500 }
501
502 # update the filter results in the multi item dialog
503 sub action_multi_items_update_result {
504   my $max_count = 100;
505
506   $::form->{multi_items}->{filter}->{obsolete} = 0;
507
508   my $count = $_[0]->multi_items_models->count;
509
510   if ($count == 0) {
511     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
512     $_[0]->render($text, { layout => 0 });
513   } elsif ($count > $max_count) {
514     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
515     $_[0]->render($text, { layout => 0 });
516   } else {
517     my $multi_items = $_[0]->multi_items_models->get;
518     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
519                   multi_items => $multi_items);
520   }
521 }
522
523 # add item rows for multiple items at once
524 sub action_add_multi_items {
525   my ($self) = @_;
526
527   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
528   return $self->js->render() unless scalar @form_attr;
529
530   my @items;
531   foreach my $attr (@form_attr) {
532     my $item = _new_item($self->order, $attr);
533     push @items, $item;
534     if ( $item->part->is_assortment ) {
535       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
536         my $attr = { parts_id => $assortment_item->parts_id,
537                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
538                      unit     => $assortment_item->unit,
539                      description => $assortment_item->part->description,
540                    };
541         my $item = _new_item($self->order, $attr);
542
543         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
544         $item->discount(1) unless $assortment_item->charge;
545         push @items, $item;
546       }
547     }
548   }
549   $self->order->add_items(@items);
550
551   $self->_recalc();
552
553   foreach my $item (@items) {
554     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
555     my $row_as_html = $self->p->render('order/tabs/_row',
556                                        ITEM              => $item,
557                                        ID                => $item_id,
558                                        TYPE              => $self->type,
559                                        ALL_PRICE_FACTORS => $self->all_price_factors
560     );
561
562     $self->js->append('#row_table_id', $row_as_html);
563   }
564
565   $self->js
566     ->run('kivi.Order.close_multi_items_dialog')
567     ->run('kivi.Order.init_row_handlers')
568     ->run('kivi.Order.row_table_scroll_down')
569     ->run('kivi.Order.renumber_positions')
570     ->focus('#add_item_parts_id_name');
571
572   $self->_js_redisplay_amounts_and_taxes;
573   $self->js->render();
574 }
575
576 # recalculate all linetotals, amounts and taxes and redisplay them
577 sub action_recalc_amounts_and_taxes {
578   my ($self) = @_;
579
580   $self->_recalc();
581
582   $self->_js_redisplay_line_values;
583   $self->_js_redisplay_amounts_and_taxes;
584   $self->js->render();
585 }
586
587 # redisplay item rows if they are sorted by an attribute
588 sub action_reorder_items {
589   my ($self) = @_;
590
591   my %sort_keys = (
592     partnumber  => sub { $_[0]->part->partnumber },
593     description => sub { $_[0]->description },
594     qty         => sub { $_[0]->qty },
595     sellprice   => sub { $_[0]->sellprice },
596     discount    => sub { $_[0]->discount },
597   );
598
599   my $method = $sort_keys{$::form->{order_by}};
600   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
601   if ($::form->{sort_dir}) {
602     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
603   } else {
604     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
605   }
606   $self->js
607     ->run('kivi.Order.redisplay_items', \@to_sort)
608     ->render;
609 }
610
611 # show the popup to choose a price/discount source
612 sub action_price_popup {
613   my ($self) = @_;
614
615   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
616   my $item = $self->order->items_sorted->[$idx];
617
618   $self->render_price_dialog($item);
619 }
620
621 # get the longdescription for an item if the dialog to enter/change the
622 # longdescription was opened and the longdescription is empty
623 #
624 # If this item is new, get the longdescription from Part.
625 # Otherwise get it from OrderItem.
626 sub action_get_item_longdescription {
627   my $longdescription;
628
629   if ($::form->{item_id}) {
630     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
631   } elsif ($::form->{parts_id}) {
632     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
633   }
634   $_[0]->render(\ $longdescription, { type => 'text' });
635 }
636
637 # load the second row for one or more items
638 #
639 # This action gets the html code for all items second rows by rendering a template for
640 # the second row and sets the html code via client js.
641 sub action_load_second_rows {
642   my ($self) = @_;
643
644   $self->_recalc() if $self->order->is_sales; # for margin calculation
645
646   foreach my $item_id (@{ $::form->{item_ids} }) {
647     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
648     my $item = $self->order->items_sorted->[$idx];
649
650     $self->_js_load_second_row($item, $item_id, 0);
651   }
652
653   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
654
655   $self->js->render();
656 }
657
658 sub _js_load_second_row {
659   my ($self, $item, $item_id, $do_parse) = @_;
660
661   if ($do_parse) {
662     # Parse values from form (they are formated while rendering (template)).
663     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
664     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
665     foreach my $var (@{ $item->cvars_by_config }) {
666       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
667     }
668     $item->parse_custom_variable_values;
669   }
670
671   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
672
673   $self->js
674     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
675     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
676 }
677
678 sub _js_redisplay_line_values {
679   my ($self) = @_;
680
681   my $is_sales = $self->order->is_sales;
682
683   # sales orders with margins
684   my @data;
685   if ($is_sales) {
686     @data = map {
687       [
688        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
689        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
690        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
691       ]} @{ $self->order->items_sorted };
692   } else {
693     @data = map {
694       [
695        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
696       ]} @{ $self->order->items_sorted };
697   }
698
699   $self->js
700     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
701 }
702
703 sub _js_redisplay_amounts_and_taxes {
704   my ($self) = @_;
705
706   if (scalar @{ $self->{taxes} }) {
707     $self->js->show('#taxincluded_row_id');
708   } else {
709     $self->js->hide('#taxincluded_row_id');
710   }
711
712   if ($self->order->taxincluded) {
713     $self->js->hide('#subtotal_row_id');
714   } else {
715     $self->js->show('#subtotal_row_id');
716   }
717
718   $self->js
719     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
720     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
721     ->remove('.tax_row')
722     ->insertBefore($self->build_tax_rows, '#amount_row_id');
723 }
724
725 #
726 # helpers
727 #
728
729 sub init_valid_types {
730   [ _sales_order_type(), _purchase_order_type() ];
731 }
732
733 sub init_type {
734   my ($self) = @_;
735
736   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
737     die "Not a valid type for order";
738   }
739
740   $self->type($::form->{type});
741 }
742
743 sub init_cv {
744   my ($self) = @_;
745
746   my $cv = $self->type eq _sales_order_type()    ? 'customer'
747          : $self->type eq _purchase_order_type() ? 'vendor'
748          : die "Not a valid type for order";
749
750   return $cv;
751 }
752
753 sub init_p {
754   SL::Presenter->get;
755 }
756
757 sub init_order {
758   $_[0]->_make_order;
759 }
760
761 # model used to filter/display the parts in the multi-items dialog
762 sub init_multi_items_models {
763   SL::Controller::Helper::GetModels->new(
764     controller     => $_[0],
765     model          => 'Part',
766     with_objects   => [ qw(unit_obj) ],
767     disable_plugin => 'paginated',
768     source         => $::form->{multi_items},
769     sorted         => {
770       _default    => {
771         by  => 'partnumber',
772         dir => 1,
773       },
774       partnumber  => t8('Partnumber'),
775       description => t8('Description')}
776   );
777 }
778
779 sub init_all_price_factors {
780   SL::DB::Manager::PriceFactor->get_all;
781 }
782
783 sub _check_auth {
784   my ($self) = @_;
785
786   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
787
788   my $right   = $right_for->{ $self->type };
789   $right    ||= 'DOES_NOT_EXIST';
790
791   $::auth->assert($right);
792 }
793
794 # build the selection box for contacts
795 #
796 # Needed, if customer/vendor changed.
797 sub build_contact_select {
798   my ($self) = @_;
799
800   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
801     value_key  => 'cp_id',
802     title_key  => 'full_name_dep',
803     default    => $self->order->cp_id,
804     with_empty => 1,
805     style      => 'width: 300px',
806   );
807 }
808
809 # build the selection box for shiptos
810 #
811 # Needed, if customer/vendor changed.
812 sub build_shipto_select {
813   my ($self) = @_;
814
815   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
816     value_key  => 'shipto_id',
817     title_key  => 'displayable_id',
818     default    => $self->order->shipto_id,
819     with_empty => 1,
820     style      => 'width: 300px',
821   );
822 }
823
824 # build the rows for displaying taxes
825 #
826 # Called if amounts where recalculated and redisplayed.
827 sub build_tax_rows {
828   my ($self) = @_;
829
830   my $rows_as_html;
831   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
832     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
833   }
834   return $rows_as_html;
835 }
836
837
838 sub render_price_dialog {
839   my ($self, $record_item) = @_;
840
841   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
842
843   $self->js
844     ->run(
845       'kivi.io.price_chooser_dialog',
846       t8('Available Prices'),
847       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
848     )
849     ->reinit_widgets;
850
851 #   if (@errors) {
852 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
853 #     $self->js->show('#dialog_flash_error');
854 #   }
855
856   $self->js->render;
857 }
858
859 sub _load_order {
860   my ($self) = @_;
861
862   return if !$::form->{id};
863
864   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
865 }
866
867 # load or create a new order object
868 #
869 # And assign changes from the for to this object.
870 # If the order is loaded from db, check if items are deleted in the form,
871 # remove them form the object and collect them for removing from db on saving.
872 # Then create/update items from form (via _make_item) and add them.
873 sub _make_order {
874   my ($self) = @_;
875
876   # add_items adds items to an order with no items for saving, but they cannot
877   # be retrieved via items until the order is saved. Adding empty items to new
878   # order here solves this problem.
879   my $order;
880   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
881   $order ||= SL::DB::Order->new(orderitems => []);
882
883   my $form_orderitems = delete $::form->{order}->{orderitems};
884   $order->assign_attributes(%{$::form->{order}});
885
886   # remove deleted items
887   $self->item_ids_to_delete([]);
888   foreach my $idx (reverse 0..$#{$order->orderitems}) {
889     my $item = $order->orderitems->[$idx];
890     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
891       splice @{$order->orderitems}, $idx, 1;
892       push @{$self->item_ids_to_delete}, $item->id;
893     }
894   }
895
896   my @items;
897   my $pos = 1;
898   foreach my $form_attr (@{$form_orderitems}) {
899     my $item = _make_item($order, $form_attr);
900     $item->position($pos);
901     push @items, $item;
902     $pos++;
903   }
904   $order->add_items(grep {!$_->id} @items);
905
906   return $order;
907 }
908
909 # create or update items from form
910 #
911 # Make item objects from form values. For items already existing read from db.
912 # Create a new item else. And assign attributes.
913 sub _make_item {
914   my ($record, $attr) = @_;
915
916   my $item;
917   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
918
919   my $is_new = !$item;
920
921   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
922   # they cannot be retrieved via custom_variables until the order/orderitem is
923   # saved. Adding empty custom_variables to new orderitem here solves this problem.
924   $item ||= SL::DB::OrderItem->new(custom_variables => []);
925
926   $item->assign_attributes(%$attr);
927   $item->longdescription($item->part->notes)   if $is_new && !defined $attr->{longdescription};
928   $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
929   $item->lastcost($item->part->lastcost)       if $is_new && !defined $attr->{lastcost_as_number};
930
931   return $item;
932 }
933
934 # create a new item
935 #
936 # This is used to add one item
937 sub _new_item {
938   my ($record, $attr) = @_;
939
940   my $item = SL::DB::OrderItem->new;
941   $item->assign_attributes(%$attr);
942
943   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
944   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
945
946   $item->unit($part->unit) if !$item->unit;
947
948   my $price_src;
949   if ( $part->is_assortment ) {
950     # add assortment items with price 0, as the components carry the price
951     $price_src = $price_source->price_from_source("");
952     $price_src->price(0);
953   } elsif ($item->sellprice) {
954     $price_src = $price_source->price_from_source("");
955     $price_src->price($item->sellprice);
956   } else {
957     $price_src = $price_source->best_price
958            ? $price_source->best_price
959            : $price_source->price_from_source("");
960     $price_src->price(0) if !$price_source->best_price;
961   }
962
963   my $discount_src;
964   if ($item->discount) {
965     $discount_src = $price_source->discount_from_source("");
966     $discount_src->discount($item->discount);
967   } else {
968     $discount_src = $price_source->best_discount
969                   ? $price_source->best_discount
970                   : $price_source->discount_from_source("");
971     $discount_src->discount(0) if !$price_source->best_discount;
972   }
973
974   my %new_attr;
975   $new_attr{part}                   = $part;
976   $new_attr{description}            = $part->description     if ! $item->description;
977   $new_attr{qty}                    = 1.0                    if ! $item->qty;
978   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
979   $new_attr{sellprice}              = $price_src->price;
980   $new_attr{discount}               = $discount_src->discount;
981   $new_attr{active_price_source}    = $price_src;
982   $new_attr{active_discount_source} = $discount_src;
983   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
984   $new_attr{project_id}             = $record->globalproject_id;
985   $new_attr{lastcost}               = $part->lastcost;
986
987   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
988   # they cannot be retrieved via custom_variables until the order/orderitem is
989   # saved. Adding empty custom_variables to new orderitem here solves this problem.
990   $new_attr{custom_variables} = [];
991
992   $item->assign_attributes(%new_attr);
993
994   return $item;
995 }
996
997 # recalculate prices and taxes
998 #
999 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1000 sub _recalc {
1001   my ($self) = @_;
1002
1003   # bb: todo: currency later
1004   $self->order->currency_id($::instance_conf->get_currency_id());
1005
1006   my %pat = $self->order->calculate_prices_and_taxes();
1007   $self->{taxes} = [];
1008   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1009     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1010
1011     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1012     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
1013                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1014                                 tax       => $tax });
1015   }
1016
1017   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1018 }
1019
1020 # get data for saving, printing, ..., that is not changed in the form
1021 #
1022 # Only cvars for now.
1023 sub _get_unalterable_data {
1024   my ($self) = @_;
1025
1026   foreach my $item (@{ $self->order->items }) {
1027     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1028     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1029     foreach my $var (@{ $item->cvars_by_config }) {
1030       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1031     }
1032     $item->parse_custom_variable_values;
1033   }
1034 }
1035
1036 # delete the order
1037 #
1038 # And remove related files in the spool directory
1039 sub _delete {
1040   my ($self) = @_;
1041
1042   my $errors = [];
1043   my $db     = $self->order->db;
1044
1045   $db->with_transaction(
1046     sub {
1047       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1048       $self->order->delete;
1049       my $spool = $::lx_office_conf{paths}->{spool};
1050       unlink map { "$spool/$_" } @spoolfiles if $spool;
1051
1052       1;
1053   }) || push(@{$errors}, $db->error);
1054
1055   return $errors;
1056 }
1057
1058 # save the order
1059 #
1060 # And delete items that are deleted in the form.
1061 sub _save {
1062   my ($self) = @_;
1063
1064   my $errors = [];
1065   my $db     = $self->order->db;
1066
1067   $db->with_transaction(sub {
1068     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1069     $self->order->save(cascade => 1);
1070   }) || push(@{$errors}, $db->error);
1071
1072   return $errors;
1073 }
1074
1075
1076 sub _pre_render {
1077   my ($self) = @_;
1078
1079   $self->{all_taxzones}        = SL::DB::Manager::TaxZone->get_all_sorted();
1080   $self->{all_departments}     = SL::DB::Manager::Department->get_all_sorted();
1081   $self->{all_employees}       = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1082                                                                                        deleted => 0 ] ],
1083                                                                     sort_by => 'name');
1084   $self->{all_salesmen}        = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1085                                                                                        deleted => 0 ] ],
1086                                                                     sort_by => 'name');
1087   $self->{all_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1088                                                                                       active => 1 ] ],
1089                                                                    sort_by => 'projectnumber');
1090   $self->{all_payment_terms}   = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1091                                                                                                  obsolete => 0 ] ]);
1092
1093   $self->{all_delivery_terms}  = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1094
1095   $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1096
1097   my $print_form = Form->new('');
1098   $print_form->{type}      = $self->type;
1099   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
1100   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1101   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
1102     form => $print_form,
1103     options => {dialog_name_prefix => 'print_options.',
1104                 show_headers       => 1,
1105                 no_queue           => 1,
1106                 no_postscript      => 1,
1107                 no_opendocument    => 1,
1108                 no_html            => 1},
1109   );
1110
1111   foreach my $item (@{$self->order->orderitems}) {
1112     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1113     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1114     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1115   }
1116
1117   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1118     my $webdav = SL::Webdav->new(
1119       type     => $self->type,
1120       number   => $self->order->ordnumber,
1121     );
1122     my @all_objects = $webdav->get_all_objects;
1123     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1124                                                     type => t8('File'),
1125                                                     link => File::Spec->catfile($_->full_filedescriptor),
1126                                                 } } @all_objects;
1127   }
1128
1129   $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery);
1130   $self->_setup_edit_action_bar;
1131 }
1132
1133 sub _setup_edit_action_bar {
1134   my ($self, %params) = @_;
1135
1136   my $deletion_allowed = (($self->cv eq 'customer') && $::instance_conf->get_sales_order_show_delete)
1137                       || (($self->cv eq 'vendor')   && $::instance_conf->get_purchase_order_show_delete);
1138
1139   for my $bar ($::request->layout->get('actionbar')) {
1140     $bar->add(
1141       combobox => [
1142         action => [
1143           t8('Save'),
1144           call      => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1145           accesskey => 'enter',
1146         ],
1147         action => [
1148           t8('Save and Delivery Order'),
1149           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1150         ],
1151         action => [
1152           t8('Save and Invoice'),
1153           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1154         ],
1155
1156       ], # end of combobox "Save"
1157
1158       combobox => [
1159         action => [
1160           t8('Export'),
1161         ],
1162         action => [
1163           t8('Print'),
1164           call => [ 'kivi.Order.show_print_options' ],
1165         ],
1166         action => [
1167           t8('E-mail'),
1168           call => [ 'kivi.Order.email' ],
1169         ],
1170         action => [
1171           t8('Download attachments of all parts'),
1172           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1173           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1174           only_if  => $::instance_conf->get_doc_storage,
1175         ],
1176       ], # end of combobox "Export"
1177
1178       action => [
1179         t8('Delete'),
1180         call     => [ 'kivi.Order.delete_order' ],
1181         confirm  => $::locale->text('Do you really want to delete this object?'),
1182         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1183         only_if  => $deletion_allowed,
1184       ],
1185     );
1186   }
1187 }
1188
1189 sub _create_pdf {
1190   my ($order, $pdf_ref, $params) = @_;
1191
1192   my @errors = ();
1193
1194   my $print_form = Form->new('');
1195   $print_form->{type}        = $order->type;
1196   $print_form->{formname}    = $params->{formname} || $order->type;
1197   $print_form->{format}      = $params->{format}   || 'pdf';
1198   $print_form->{media}       = $params->{media}    || 'file';
1199   $print_form->{groupitems}  = $params->{groupitems};
1200   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1201   $print_form->{language}    = $params->{language}->template_code if $print_form->{language};
1202   $print_form->{language_id} = $params->{language}->id            if $print_form->{language};
1203
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