]> wagnertech.de Git - mfinanz.git/blob - SL/Controller/DeliveryOrder.pm
Merge branch 'master' of http://wagnertech.de/git/mfinanz
[mfinanz.git] / SL / Controller / DeliveryOrder.pm
1 package SL::Controller::DeliveryOrder;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Helper::Number qw(_format_number _parse_number);
8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
9 use SL::Presenter::DeliveryOrder qw(delivery_order_status_line);
10 use SL::Locale::String qw(t8);
11 use SL::SessionFile::Random;
12 use SL::PriceSource;
13 use SL::Webdav;
14 use SL::File;
15 use SL::MIME;
16 use SL::Util qw(trim);
17 use SL::YAML;
18 use SL::DBUtils qw(selectall_hashref_query);
19 use SL::DB::History;
20 use SL::DB::Default;
21 use SL::DB::Unit;
22 use SL::DB::Order;
23 use SL::DB::Order::TypeData qw(:types);
24 use SL::DB::Part;
25 use SL::DB::PartClassification;
26 use SL::DB::PartsGroup;
27 use SL::DB::Printer;
28 use SL::DB::Language;
29 use SL::DB::Reclamation;
30 use SL::DB::Reclamation::TypeData qw(:types);
31 use SL::DB::RecordLink;
32 use SL::DB::Shipto;
33 use SL::DB::Translation;
34 use SL::DB::TransferType;
35 use SL::DB::ValidityToken;
36 use SL::DB::EmailJournal;
37 use SL::DB::Warehouse;
38 use SL::DB::Bin;
39 use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
40 use SL::DB::Helper::TypeDataProxy;
41 use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
42 use SL::DB::DeliveryOrder;
43 use SL::DB::DeliveryOrder::TypeData qw(:types);
44 use SL::DB::Manager::DeliveryOrderItem;
45 use SL::DB::DeliveryOrderItemsStock;
46 use SL::Model::Record;
47
48 use SL::Helper::CreatePDF qw(:all);
49 use SL::Helper::PrintOptions;
50 use SL::Helper::ShippedQty;
51 use SL::Helper::Inventory;
52 use SL::Helper::DateTime;
53 use SL::Helper::UserPreferences::DisplayPreferences;
54 use SL::Helper::UserPreferences::PositionsScrollbar;
55 use SL::Helper::UserPreferences::UpdatePositions;
56
57 use SL::Controller::Helper::GetModels;
58
59 use List::Util qw(first sum0);
60 use List::UtilsBy qw(sort_by uniq_by);
61 use List::MoreUtils qw(any none pairwise first_index);
62 use English qw(-no_match_vars);
63 use File::Spec;
64 use Cwd;
65 use Sort::Naturally;
66
67 use Rose::Object::MakeMethods::Generic (
68     scalar => [qw(item_ids_to_delete is_custom_shipto_to_delete)],
69     'scalar --get_set_init' => [ qw(
70       order valid_types type cv p all_price_factors search_cvpartnumber
71       show_update_button part_picker_classification_ids type_data
72       ) ],
73 );
74
75
76 # safety
77 __PACKAGE__->run_before('check_auth',
78   except => [ qw(
79     update_stock_information
80     ) ]);
81
82 __PACKAGE__->run_before('check_auth_for_edit',
83   except => [ qw(
84     update_stock_information edit
85     stock_in_out_dialog load_second_rows
86     ) ]);
87
88 __PACKAGE__->run_before('get_unalterable_data',
89   only => [ qw(
90     save save_as_new workflow_new_record workflow_invoice
91     save_and_ap_transaction print send_email
92     ) ]);
93
94 #
95 # actions
96 #
97
98 # add a new order
99 sub action_add {
100   my ($self) = @_;
101
102   $self->pre_render();
103
104   if (!$::form->{form_validity_token}) {
105     $::form->{form_validity_token} = SL::DB::ValidityToken->create(
106       scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
107     )->token;
108   }
109
110   $self->render(
111     'delivery_order/form',
112     title => $self->get_title_for('add'),
113     %{$self->{template_args}}
114   );
115 }
116
117 sub action_add_from_record {
118   my ($self) = @_;
119   my $from_type = $::form->{from_type};
120   my $from_id   = $::form->{from_id};
121
122   die "No 'from_type' was given." unless ($from_type);
123   die "No 'from_id' was given."   unless ($from_id);
124
125   my %flags = ();
126   if (defined($::form->{from_item_ids})) {
127     my %use_item = map { $_ => 1 } @{$::form->{from_item_ids}};
128     $flags{item_filter} = sub {
129       my ($item) = @_;
130       return %use_item{$item->{RECORD_ITEM_ID()}};
131     }
132   }
133
134   my $record = SL::Model::Record->get_record($from_type, $from_id);
135
136   # If we are coming from an order workflow, only consider not delivered
137   # quantities.
138   if (ref $record eq 'SL::DB::Order') {
139     # Calculate shipped qtys here to prevent calling calculate for every item
140     # via the items method.
141     SL::Helper::ShippedQty->new->calculate($record)->write_to(\@{$record->items});
142
143     my @items_with_not_delivered_qty =
144       grep {$_->qty > 0}
145       map  {$_->qty($_->qty - $_->shipped_qty); $_}
146       @{$record->items_sorted};
147
148     $flags{items} = \@items_with_not_delivered_qty;
149   }
150
151   my $delivery_order = SL::Model::Record->new_from_workflow($record, $self->type, %flags);
152   $self->order($delivery_order);
153   $self->reinit_after_new_order();
154
155   $self->action_add;
156 }
157
158 sub action_add_from_email_journal {
159   my ($self) = @_;
160   die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
161
162   $self->action_add();
163 }
164
165 sub action_edit_with_email_journal_workflow {
166   my ($self) = @_;
167   die "No 'email_journal_id' was given." unless ($::form->{email_journal_id});
168   $::form->{workflow_email_journal_id}    = delete $::form->{email_journal_id};
169   $::form->{workflow_email_attachment_id} = delete $::form->{email_attachment_id};
170   $::form->{workflow_email_callback}      = delete $::form->{callback};
171
172   $self->action_edit();
173 }
174
175 # edit an existing order
176 sub action_edit {
177   my ($self) = @_;
178   die "No 'id' was given." unless $::form->{id};
179
180   $self->load_order;
181
182   $self->pre_render();
183   $self->render(
184     'delivery_order/form',
185     title => $self->get_title_for('edit'),
186     %{$self->{template_args}}
187   );
188 }
189
190 # delete the order
191 sub action_delete {
192   my ($self) = @_;
193
194   SL::Model::Record->delete($self->order);
195   flash_later('info', $self->type_data->text("delete"));
196
197   my @redirect_params = (
198     action => 'add',
199     type   => $self->type,
200   );
201
202   $self->redirect_to(@redirect_params);
203 }
204
205 # save the order
206 sub action_save {
207   my ($self) = @_;
208
209   if ( $self->order->delivered ) {
210     $self->js->flash('error', t8('This record has already been delivered.'));
211     return $self->js->render();
212   }
213
214   $self->save();
215
216   flash_later('info', $self->type_data->text("saved"));
217
218   my @redirect_params;
219   if ($::form->{back_to_caller}) {
220     @redirect_params = $::form->{callback} ? ($::form->{callback})
221                                            : (controller => 'LoginScreen', action => 'user_login');
222
223   } else {
224     @redirect_params = (
225       action   => 'edit',
226       type     => $self->type,
227       id       => $self->order->id,
228       callback => $::form->{callback},
229     );
230   }
231
232   $self->redirect_to(@redirect_params);
233 }
234
235 # save the order as new document an open it for edit
236 sub action_save_as_new {
237   my ($self) = @_;
238
239   my $order = $self->order;
240
241   if (!$order->id) {
242     $self->js->flash('error', t8('This object has not been saved yet.'));
243     return $self->js->render();
244   }
245
246   my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
247
248   # Create new record from current one
249   my $new_order = SL::Model::Record->clone_for_save_as_new($saved_order, $order);
250   $self->order($new_order);
251
252   if (!$::form->{form_validity_token}) {
253     $::form->{form_validity_token} = SL::DB::ValidityToken->create(
254       scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE()
255     )->token;
256   }
257
258   # save
259   $self->action_save();
260 }
261
262 # close a already saved order (potentially already delivered)
263 sub action_close_order {
264   my ($self) = @_;
265
266   $self->order->update_attributes(
267     closed => 1
268   );
269
270   $self->js
271     ->flash("info", t8("The record has been closed."))
272     ->run('kivi.ActionBar.setDisabled', '#close_order',
273           t8('This record has already been closed.'))
274     ->html('#data-status-line', delivery_order_status_line($self->order))
275     ->render
276 }
277
278 # print the order
279 #
280 # This is called if "print" is pressed in the print dialog.
281 # If PDF creation was requested and succeeded, the pdf is offered for download
282 # via send_file (which uses ajax in this case).
283 sub action_print {
284   my ($self) = @_;
285
286   if ( !$self->order->delivered ) {
287     $self->save();
288     $self->js_reset_order_and_item_ids_after_save;
289   }
290
291   my $redirect_url = $self->url_for(
292     action => 'edit',
293     type   => $self->type,
294     id     => $self->order->id,
295   );
296
297   my $format      = $::form->{print_options}->{format};
298   my $media       = $::form->{print_options}->{media};
299   my $formname    = $::form->{print_options}->{formname};
300   my $copies      = $::form->{print_options}->{copies};
301   my $groupitems  = $::form->{print_options}->{groupitems};
302   my $printer_id  = $::form->{print_options}->{printer_id};
303
304   # only pdf and opendocument by now
305   if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
306     flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
307     return $self->js->redirect_to($redirect_url)->render;
308   }
309
310   # only screen or printer by now
311   if (none { $media eq $_ } qw(screen printer)) {
312     flash_later('error', t8('Media \'#1\' is not supported yet/anymore.', $media));
313     return $self->js->redirect_to($redirect_url)->render;
314   }
315
316   # create a form for generate_attachment_filename
317   my $form   = Form->new;
318   $form->{$self->nr_key()}  = $self->order->number;
319   $form->{type}             = $self->type;
320   $form->{format}           = $format;
321   $form->{formname}         = $formname;
322   $form->{language}         =
323     '_' . $self->order->language->template_code if $self->order->language;
324   my $pdf_filename          = $form->generate_attachment_filename();
325   my $pdf;
326   my @errors = generate_pdf($self->order, \$pdf, {
327       format     => $format,
328       formname   => $formname,
329       language   => $self->order->language,
330       printer_id => $printer_id,
331       groupitems => $groupitems
332     });
333   if (scalar @errors) {
334     flash_later('error', t8('Generating the document failed: #1', $errors[0]));
335     return $self->js->redirect_to($redirect_url)->render;
336   }
337
338   if ($media eq 'screen') {
339     # screen/download
340     flash_later('info', t8('The document has been created.'));
341     $self->send_file(
342       \$pdf,
343       type         => SL::MIME->mime_type_from_ext($pdf_filename),
344       name         => $pdf_filename,
345       js_no_render => 1,
346     );
347
348   } elsif ($media eq 'printer') {
349     # printer
350     my $printer_id = $::form->{print_options}->{printer_id};
351     SL::DB::Printer->new(id => $printer_id)->load->print_document(
352       copies  => $copies,
353       content => $pdf,
354     );
355
356     flash_later('info', t8('The document has been printed.'));
357   }
358
359   my @warnings = store_pdf_to_webdav_and_filemanagement(
360     $self->order, $pdf, $pdf_filename, $formname
361   );
362   if (scalar @warnings) {
363     flash_later('warning', $_) for @warnings;
364   }
365
366   $self->save_history('PRINTED');
367
368   $self->js->redirect_to($redirect_url)->render;
369 }
370
371 sub action_preview_pdf {
372   my ($self) = @_;
373
374   if ( !$self->order->delivered ) {
375     $self->save();
376     $self->js_reset_order_and_item_ids_after_save;
377   }
378
379   my $redirect_url = $self->url_for(
380     action => 'edit',
381     type   => $self->type,
382     id     => $self->order->id,
383   );
384
385   my $format      = 'pdf';
386   my $media       = 'screen';
387   my $formname    = $self->type;
388
389   # only pdf
390   # create a form for generate_attachment_filename
391   my $form   = Form->new;
392   $form->{$self->nr_key()}  = $self->order->number;
393   $form->{type}             = $self->type;
394   $form->{format}           = $format;
395   $form->{formname}         = $formname;
396   $form->{language}         =
397     '_' . $self->order->language->template_code if $self->order->language;
398   my $pdf_filename          = $form->generate_attachment_filename();
399
400   my $pdf;
401   my @errors = generate_pdf($self->order, \$pdf, {
402       format     => $format,
403       formname   => $formname,
404       language   => $self->order->language,
405     });
406   if (scalar @errors) {
407     flash_later('error', t8('Conversion to PDF failed: #1', $errors[0]));
408     return $self->js->redirect_to($redirect_url)->render;
409   }
410   $self->save_history('PREVIEWED');
411   flash_later('info', t8('The PDF has been previewed'));
412   # screen/download
413   $self->send_file(
414     \$pdf,
415     type         => SL::MIME->mime_type_from_ext($pdf_filename),
416     name         => $pdf_filename,
417     js_no_render => 1,
418   );
419   $self->js->redirect_to($redirect_url)->render;
420 }
421
422 # open the email dialog
423 sub action_save_and_show_email_dialog {
424   my ($self) = @_;
425
426   if (!$self->order->delivered) {
427     $self->save();
428     $self->js_reset_order_and_item_ids_after_save;
429   }
430
431   my $cv = $self->order->customervendor
432     or return $self->js->flash('error',
433       $self->cv eq 'customer' ?
434            t8('Cannot send E-mail without customer given')
435          : t8('Cannot send E-mail without vendor given')
436     )->render($self);
437
438   my $form = Form->new;
439   $form->{$self->nr_key()}  = $self->order->number;
440   $form->{cusordnumber}     = $self->order->cusordnumber;
441   $form->{formname}         = $self->type;
442   $form->{type}             = $self->type;
443   $form->{language}         =
444     '_' . $self->order->language->template_code if $self->order->language;
445   $form->{language_id}      =
446     $self->order->language->id                  if $self->order->language;
447   $form->{format}           = 'pdf';
448   $form->{cp_id}            =
449     $self->order->contact->cp_id if $self->order->contact;
450
451   my $email_form;
452   $email_form->{to} =
453        ($self->order->contact ? $self->order->contact->cp_email : undef)
454     || ($cv->is_customer ? $cv->delivery_order_mail : undef)
455     ||  $cv->email;
456   $email_form->{cc}  = $cv->cc;
457   $email_form->{bcc} = join ', ', grep $_, $cv->bcc;
458   # Todo: get addresses from shipto, if any
459   $email_form->{subject}             = $form->generate_email_subject();
460   $email_form->{attachment_filename} = $form->generate_attachment_filename();
461   $email_form->{message}             = $form->generate_email_body();
462   $email_form->{js_send_function}    = 'kivi.DeliveryOrder.send_email()';
463
464   my %files = $self->get_files_for_email_dialog();
465
466   my @employees_with_email = grep {
467     my $user = SL::DB::Manager::AuthUser->find_by(login => $_->login);
468     $user && !!trim($user->get_config_value('email'));
469   } @{ SL::DB::Manager::Employee->get_all_sorted(query => [ deleted => 0 ]) };
470
471   my $dialog_html = $self->render(
472     'common/_send_email_dialog', { output => 0 },
473     email_form  => $email_form,
474     show_bcc    => $::auth->assert('email_bcc', 'may fail'),
475     FILES       => \%files,
476     is_customer => $self->type_data->properties("is_customer"),
477     ALL_EMPLOYEES => \@employees_with_email,
478     ALL_PARTNER_EMAIL_ADDRESSES => $cv->get_all_email_addresses(),
479   );
480
481   $self->js
482     ->run('kivi.DeliveryOrder.show_email_dialog', $dialog_html)
483     ->reinit_widgets
484     ->render($self);
485 }
486
487 # send email
488 sub action_send_email {
489   my ($self) = @_;
490
491   if ( !$self->order->delivered ) {
492     eval {
493       $self->save();
494       1;
495     } or do {
496       $self->js->run('kivi.Order.close_email_dialog');
497       die $EVAL_ERROR;
498     };
499   }
500
501   my @redirect_params = (
502     action => 'edit',
503     type   => $self->type,
504     id     => $self->order->id,
505   );
506
507   # Set the error handler to reload the document and display errors later,
508   # because the document is already saved and saving can have some side effects
509   # such as generating a document number, project number or record links,
510   # which will be up to date when the document is reloaded.
511   # Hint: Do not use "die" here and try to catch exceptions in subroutine
512   # calls. You should use "$::form->error" which respects the error handler.
513   local $::form->{__ERROR_HANDLER} = sub {
514       flash_later('error', $_[0]);
515       $self->redirect_to(@redirect_params);
516       $::dispatcher->end_request;
517   };
518
519   # move $::form->{email_form} to $::form
520   my $email_form  = delete $::form->{email_form};
521
522   if ($email_form->{additional_to}) {
523     $email_form->{to} = join ', ', grep { $_ } $email_form->{to}, @{$email_form->{additional_to}};
524     delete $email_form->{additional_to};
525   }
526
527   my %field_names = (to => 'email');
528   $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
529
530   # for Form::cleanup which may be called in Form::send_email
531   $::form->{cwd}    = getcwd();
532   $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
533
534   $::form->{$_}     = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
535   $::form->{media}  = 'email';
536
537   $::form->{attachment_policy} //= '';
538
539   # Is an old file version available?
540   my $attfile;
541   if ($::form->{attachment_policy} eq 'old_file') {
542     $attfile = SL::File->get_all(
543       object_id   => $self->order->id,
544       object_type => $::form->{formname},
545       file_type   => 'document',
546       print_variant => $::form->{formname},
547     );
548   }
549
550   if (   $::form->{attachment_policy} ne 'no_file'
551     && !($::form->{attachment_policy} eq 'old_file' && $attfile)) {
552     my $pdf;
553     my @errors = generate_pdf($self->order, \$pdf, {
554         media      => $::form->{media},
555         format     => $::form->{print_options}->{format},
556         formname   => $::form->{print_options}->{formname},
557         language   => $self->order->language,
558         printer_id => $::form->{print_options}->{printer_id},
559         groupitems => $::form->{print_options}->{groupitems}},
560     );
561     if (scalar @errors) {
562       $::form->error(t8('Generating the document failed: #1', $errors[0]));
563     }
564
565     my @warnings = store_pdf_to_webdav_and_filemanagement(
566       $self->order, $pdf, $::form->{attachment_filename}, $::form->{formname}
567     );
568     if (scalar @warnings) {
569       flash_later('warning', $_) for @warnings;
570     }
571
572     my $sfile = SL::SessionFile::Random->new(mode => "w");
573     $sfile->fh->print($pdf);
574     $sfile->fh->close;
575
576     $::form->{tmpfile} = $sfile->file_name;
577     $::form->{tmpdir}  = $sfile->get_path; # for Form::cleanup which may be
578                                            # called in Form::send_email
579   }
580
581   $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a
582                                     # linked record to the mail
583   $::form->send_email(\%::myconfig, 'pdf');
584
585   $self->save_history('MAILED');
586   flash_later('info', t8('The email has been sent.'));
587
588   # internal notes unless no email journal
589   unless ($::instance_conf->get_email_journal) {
590     my $intnotes = $self->order->intnotes;
591     $intnotes   .= "\n\n" if $self->order->intnotes;
592     $intnotes   .= t8('[email]')                                . "\n";
593     $intnotes   .= t8('Date')       . ": " .
594       $::locale->format_date_object(
595         DateTime->now_local, precision => 'seconds'
596       ) . "\n";
597     $intnotes   .= t8('To (email)') . ": " . $::form->{email}   . "\n";
598     $intnotes   .= t8('Cc')         . ": " . $::form->{cc}      . "\n"    if $::form->{cc};
599     $intnotes   .= t8('Bcc')        . ": " . $::form->{bcc}     . "\n"    if $::form->{bcc};
600     $intnotes   .= t8('Subject')    . ": " . $::form->{subject} . "\n\n";
601     $intnotes   .= t8('Message')    . ": " . SL::HTML::Util->strip($::form->{message});
602
603     $self->order->update_attributes(intnotes => $intnotes);
604   }
605
606   $self->redirect_to(@redirect_params);
607 }
608
609 sub action_workflow_new_record {
610   my ($self) = @_;
611   my $to_type = $::form->{to_type};
612   my $to_controller = get_object_name_from_type($to_type);
613
614   my %additional_params = ();
615   if ($::form->{only_selected_item_positions}) { # ids can be unset before save
616     my $item_positions = $::form->{selected_item_positions} || [];
617     my @from_item_ids = map { $self->order->items_sorted->[$_]->id } @$item_positions;
618     $additional_params{from_item_ids} = \@from_item_ids;
619   }
620
621   flash_later('info', $self->type_data->text('saved'));
622
623   $self->redirect_to(
624     controller => $to_controller,
625     action     => 'add_from_record',
626     type       => $to_type,
627     from_id    => $self->order->id,
628     from_type  => $self->order->type,
629     email_journal_id    => $::form->{workflow_email_journal_id},
630     email_attachment_id => $::form->{workflow_email_attachment_id},
631     callback            => $::form->{workflow_email_callback},
632     %additional_params,
633   );
634 }
635
636 # save the order and redirect to the frontend subroutine for a new
637 # invoice
638 sub action_workflow_invoice {
639   my ($self) = @_;
640
641   $self->redirect_to(
642     controller => 'do.pl',
643     action     => 'invoice_from_delivery_order_controller',
644     from_id    => $self->order->id,
645     email_journal_id    => $::form->{workflow_email_journal_id},
646     email_attachment_id => $::form->{workflow_email_attachment_id},
647     callback            => $::form->{workflow_email_callback},
648   );
649 }
650
651 # set form elements in respect to a changed customer or vendor
652 #
653 # This action is called on an change of the customer/vendor picker.
654 sub action_customer_vendor_changed {
655   my ($self) = @_;
656
657   $self->order(
658     SL::Model::Record->update_after_customer_vendor_change($self->order)
659   );
660
661   my $cv_method = $self->cv;
662
663   if ( $self->order->$cv_method->contacts
664     && scalar @{ $self->order->$cv_method->contacts } > 0) {
665     $self->js->show('#cp_row');
666   } else {
667     $self->js->hide('#cp_row');
668   }
669
670   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
671     $self->js->show('#shipto_selection');
672   } else {
673     $self->js->hide('#shipto_selection');
674   }
675
676   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
677
678   $self->js
679     ->replaceWith('#order_cp_id',            $self->build_contact_select)
680     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
681     ->replaceWith('#shipto_inputs  ',        $self->build_shipto_inputs)
682     ->replaceWith('#business_info_row',      $self->build_business_info_row)
683     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
684     ->val(        '#order_taxincluded',      $self->order->taxincluded)
685     ->val(        '#order_currency_id',      $self->order->currency_id)
686     ->val(        '#order_payment_id',       $self->order->payment_id)
687     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
688     ->val(        '#order_intnotes',         $self->order->intnotes)
689     ->val(        '#order_language_id',      $self->order->$cv_method->language_id)
690     ->focus(      '#order_' . $self->cv . '_id')
691     ->run('kivi.DeliveryOrder.update_exchangerate');
692
693   $self->js_redisplay_cvpartnumbers;
694   $self->js->render();
695 }
696
697 # called if a unit in an existing item row is changed
698 sub action_unit_changed {
699   my ($self) = @_;
700
701   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
702   my $item = $self->order->items_sorted->[$idx];
703
704   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
705   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
706
707   $self->js->run(
708     'kivi.DeliveryOrder.update_sellprice',
709     $::form->{item_id},
710     $item->sellprice_as_number
711   );
712   $self->js_redisplay_line_values;
713   $self->js->render();
714 }
715
716 # add an item row for a new item entered in the input row
717 sub action_add_item {
718   my ($self) = @_;
719
720   delete $::form->{add_item}->{create_part_type};
721
722   my $form_attr = $::form->{add_item};
723
724   return unless $form_attr->{parts_id};
725
726   my $item = new_item($self->order, $form_attr);
727
728   $self->order->add_items($item);
729
730   $self->get_item_cvpartnumber($item);
731
732   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
733   my $row_as_html = $self->p->render('delivery_order/tabs/_row',
734                                      ITEM => $item,
735                                      ID   => $item_id,
736                                      SELF => $self,
737                                      in_out => $self->type_data->properties("transfer"),
738   );
739
740   if ($::form->{insert_before_item_id}) {
741     $self->js
742       ->before(
743         '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
744         $row_as_html
745       );
746   } else {
747     $self->js
748       ->append('#row_table_id', $row_as_html);
749   }
750
751   if ( $item->part->is_assortment ) {
752     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
753     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
754       my $attr = {
755         parts_id => $assortment_item->parts_id,
756         qty      => $assortment_item->qty *
757           $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
758         unit     => $assortment_item->unit,
759         description => $assortment_item->part->description,
760       };
761       my $item = new_item($self->order, $attr);
762
763       # set discount to 100% if item isn't supposed to be charged, overwriting
764       # any customer discount
765       $item->discount(1) unless $assortment_item->charge;
766
767       $self->order->add_items( $item );
768       $self->get_item_cvpartnumber($item);
769       my $item_id = join('_',
770         'new',
771         Time::HiRes::gettimeofday(),
772         int rand 1000000000000
773       );
774       my $row_as_html = $self->p->render('delivery_order/tabs/_row',
775                                          ITEM => $item,
776                                          ID   => $item_id,
777                                          SELF => $self,
778       );
779       if ($::form->{insert_before_item_id}) {
780         $self->js
781           ->before(
782             '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
783             $row_as_html
784           );
785       } else {
786         $self->js
787           ->append('#row_table_id', $row_as_html);
788       }
789     };
790   };
791
792   $self->js
793     ->val('.add_item_input', '')
794     ->run('kivi.DeliveryOrder.init_row_handlers')
795     ->run('kivi.DeliveryOrder.renumber_positions')
796     ->focus('#add_item_parts_id_name');
797
798   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
799
800   $self->js->render();
801 }
802
803 # add item rows for multiple items at once
804 sub action_add_multi_items {
805   my ($self) = @_;
806
807   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
808   return $self->js->render() unless scalar @form_attr;
809
810   my @items;
811   foreach my $attr (@form_attr) {
812     my $item = new_item($self->order, $attr);
813     push @items, $item;
814     if ( $item->part->is_assortment ) {
815       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
816         my $attr = {
817           parts_id => $assortment_item->parts_id,
818           qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
819           unit     => $assortment_item->unit,
820           description => $assortment_item->part->description,
821         };
822         my $item = new_item($self->order, $attr);
823
824         # set discount to 100% if item isn't supposed to be charged, overwriting
825         # any customer discount
826         $item->discount(1) unless $assortment_item->charge;
827         push @items, $item;
828       }
829     }
830   }
831   $self->order->add_items(@items);
832
833   foreach my $item (@items) {
834     $self->get_item_cvpartnumber($item);
835     my $item_id = join('_',
836       'new',
837       Time::HiRes::gettimeofday(),
838       int rand 1000000000000
839     );
840     my $row_as_html = $self->p->render('delivery_order/tabs/_row',
841       ITEM => $item,
842       ID   => $item_id,
843       SELF => $self,
844       in_out => $self->type_data->properties("transfer"),
845     );
846
847     if ($::form->{insert_before_item_id}) {
848       $self->js
849         ->before(
850           '.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')',
851           $row_as_html
852         );
853     } else {
854       $self->js
855         ->append('#row_table_id', $row_as_html);
856     }
857   }
858
859   $self->js
860     ->run('kivi.Part.close_picker_dialogs')
861     ->run('kivi.DeliveryOrder.init_row_handlers')
862     ->run('kivi.DeliveryOrder.renumber_positions')
863     ->focus('#add_item_parts_id_name');
864
865   $self->js->run('kivi.DeliveryOrder.row_table_scroll_down') if !$::form->{insert_before_item_id};
866
867   $self->js->render();
868 }
869
870 sub action_update_exchangerate {
871   my ($self) = @_;
872
873   my $data = {
874     is_standard   => $self->order->currency_id == $::instance_conf->get_currency_id,
875     currency_name => $self->order->currency->name,
876   };
877
878   $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
879 }
880
881 # redisplay item rows if they are sorted by an attribute
882 sub action_reorder_items {
883   my ($self) = @_;
884
885   my %sort_keys = (
886     partnumber   => sub { $_[0]->part->partnumber },
887     description  => sub { $_[0]->description },
888     qty          => sub { $_[0]->qty },
889     sellprice    => sub { $_[0]->sellprice },
890     discount     => sub { $_[0]->discount },
891     cvpartnumber => sub { $_[0]->{cvpartnumber} },
892   );
893
894   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
895
896   my $method = $sort_keys{$::form->{order_by}};
897   my @to_sort =
898     map { { old_pos => $_->position, order_by => $method->($_) } }
899     @{ $self->order->items_sorted };
900   if ($::form->{sort_dir}) {
901     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
902       @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
903     } else {
904       @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
905     }
906   } else {
907     if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
908       @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
909     } else {
910       @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
911     }
912   }
913   $self->js
914     ->run('kivi.DeliveryOrder.redisplay_items', \@to_sort)
915     ->render;
916 }
917
918 # save the order in a session variable and redirect to the part controller
919 sub action_create_part {
920   my ($self) = @_;
921
922   my $previousform = $::auth->save_form_in_session(non_scalars => 1);
923
924   my $callback     = $self->url_for(
925     action       => 'return_from_create_part',
926     type         => $self->type, # type is needed for check_auth on return
927     previousform => $previousform,
928   );
929
930   flash_later('info',
931     t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.')
932   );
933
934   my @redirect_params = (
935     controller    => 'Part',
936     action        => 'add',
937     part_type     => $::form->{add_item}->{create_part_type},
938     callback      => $callback,
939     inline_create => 1,
940   );
941
942   $self->redirect_to(@redirect_params);
943 }
944
945 sub action_return_from_create_part {
946   my ($self) = @_;
947
948   $self->{created_part} = SL::DB::Part->new(
949     id => delete $::form->{new_parts_id}
950   )->load if $::form->{new_parts_id};
951
952   $::auth->restore_form_from_session(delete $::form->{previousform});
953
954   $self->order($self->init_order);
955   $self->reinit_after_new_order();
956
957   if ($self->order->id) {
958     $self->pre_render();
959     $self->render(
960       'delivery_order/form',
961       title => $self->get_title_for('edit'),
962       %{$self->{template_args}}
963     );
964   } else {
965     $self->action_add;
966   }
967
968 }
969
970 sub action_stock_in_out_dialog {
971   my ($self) = @_;
972
973   my $part        = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
974   my $unit        = SL::DB::Unit->load_cached($::form->{unit}) or die "need unit";
975   my $stock       = $::form->{stock};
976   my $row         = $::form->{row};
977   my $item_id     = $::form->{item_id};
978   my $qty         = _parse_number($::form->{qty_as_number});
979   my $row_ui_id   = $::form->{row_ui_id};
980   my $next_button = $::form->{next_button} eq 'true';
981
982   my $inout = $self->type_data->properties("transfer");
983
984   my @contents   = DO->get_item_availability(parts_id => $part->id);
985   my $stock_info = DO->unpack_stock_information(packed => $stock);
986
987   $self->merge_stock_data($stock_info, \@contents, $part, $unit);
988
989   my $delivered = $self->order->delivered;
990   $self->render("delivery_order/stock_dialog", { layout => 0 },
991     WHCONTENTS  => \@contents,
992     STOCK_INFO  => $stock_info,
993     WAREHOUSES  => !$delivered ? SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]], with_objects=> ["bins",]) : [],
994     part        => $part,
995     do_qty      => $qty,
996     do_unit     => $unit->name,
997     delivered   => $self->order->delivered,
998     row         => $row,
999     item_id     => $item_id,
1000     in_out      => $inout,
1001     row_ui_id   => $row_ui_id,
1002     next_button => $next_button,
1003   );
1004 }
1005
1006 sub action_add_stock_in_line_to_dialog {
1007   my ($self) = @_;
1008
1009   my $do_qty       = _parse_number($::form->{do_qty});
1010   my $qty_sum   = $::form->{qty_sum};
1011   my $row_count = $::form->{row_count};
1012   my $part      = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
1013
1014   my $row_as_html = $self->p->render('delivery_order/stock_dialog/_stock_in_new_row',
1015     WAREHOUSES => SL::DB::Manager::Warehouse->get_all(with_objects=> ["bins",]),
1016     PART => $part,
1017     pos  => $row_count + 1,
1018     remaining_qty => $do_qty - $qty_sum,
1019   );
1020
1021   $self->js->append('#stock-in-out-table tbody', $row_as_html)->render();
1022 }
1023
1024 sub action_update_stock_information {
1025   my ($self) = @_;
1026
1027   my $stock_info = $::form->{stock_info};
1028   my $unit = $::form->{unit};
1029   my $yaml = SL::YAML::Dump($stock_info);
1030   my $stock_qty = $self->calculate_stock_in_out_from_stock_info($unit, $stock_info);
1031
1032   my $response = {
1033     stock_info => $yaml,
1034     stock_qty => $stock_qty,
1035   };
1036   $self->render(
1037     \ SL::JSON::to_json($response),
1038     { layout => 0, type => 'json', process => 0 }
1039   );
1040 }
1041
1042 sub merge_stock_data {
1043   my ($self, $stock_info, $contents, $part, $unit) = @_;
1044   # TODO rewrite to mapping
1045
1046   if (!$self->order->delivered) {
1047     for my $row (@$contents) {
1048       # row here is in parts units. stock is in item units
1049       $row->{available_qty} = _format_number(
1050         $part->unit_obj->convert_to($row->{qty}, $unit)
1051       );
1052
1053       for my $sinfo (@{ $stock_info }) {
1054         next if $row->{bin_id}       != $sinfo->{bin_id} ||
1055                 $row->{warehouse_id} != $sinfo->{warehouse_id} ||
1056                 $row->{chargenumber} ne $sinfo->{chargenumber} ||
1057                 $row->{bestbefore}   ne $sinfo->{bestbefore};
1058
1059         $row->{"stock_$_"} = $sinfo->{$_}
1060           for qw(qty unit error delivery_order_items_stock_id);
1061       }
1062     }
1063
1064   } else {
1065     for my $sinfo (@{ $stock_info }) {
1066       my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
1067       $sinfo->{warehousedescription} = $bin->warehouse->description;
1068       $sinfo->{bindescription}       = $bin->description;
1069       map { $sinfo->{"stock_$_"}      = $sinfo->{$_} } qw(qty unit);
1070     }
1071   }
1072 }
1073
1074 # load the second row for one or more items
1075 #
1076 # This action gets the html code for all items second rows by rendering a template for
1077 # the second row and sets the html code via client js.
1078 sub action_load_second_rows {
1079   my ($self) = @_;
1080
1081   foreach my $item_id (@{ $::form->{item_ids} }) {
1082     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1083     my $item = $self->order->items_sorted->[$idx];
1084
1085     $self->js_load_second_row($item, $item_id, 0);
1086   }
1087
1088   # for lastcosts change-callback
1089   $self->js->run('kivi.DeliveryOrder.init_row_handlers') if $self->order->is_sales;
1090
1091   $self->js->render();
1092 }
1093
1094 # update description, notes and sellprice from master data
1095 sub action_update_row_from_master_data {
1096   my ($self) = @_;
1097
1098   foreach my $item_id (@{ $::form->{item_ids} }) {
1099     my $idx   = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
1100     my $item  = $self->order->items_sorted->[$idx];
1101     my $texts = get_part_texts($item->part, $self->order->language_id);
1102
1103     $item->description($texts->{description});
1104     $item->longdescription($texts->{longdescription});
1105
1106     my ($price_src, undef) = SL::Model::Record->get_best_price_and_discount_source($self->order, $item, ignore_given => 1);
1107     $item->sellprice($price_src->price);
1108     $item->active_price_source($price_src);
1109
1110     $self->js
1111       ->run('kivi.DeliveryOrder.update_sellprice', $item_id, $item->sellprice_as_number)
1112       ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1113       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1114       ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1115
1116     if ($self->search_cvpartnumber) {
1117       $self->get_item_cvpartnumber($item);
1118       $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1119     }
1120   }
1121
1122   $self->js_redisplay_line_values;
1123
1124   $self->js->render();
1125 }
1126
1127 sub action_transfer_stock {
1128   my ($self, $default_transfer) = @_;
1129
1130   if ($self->order->delivered) {
1131     return $self->js->flash("error",
1132       t8('The parts for this order have already been transferred')
1133     )->render;
1134   }
1135
1136   my $inout = $self->type_data->properties('transfer');
1137
1138   $self->save;
1139
1140   my $order = $self->order;
1141
1142   # TODO move to type data
1143   my $trans_type = $inout eq 'in'
1144     ? SL::DB::Manager::TransferType->find_by(
1145         direction => "in", description => "stock")
1146     : SL::DB::Manager::TransferType->find_by(
1147         direction => "out", description => "shipped");
1148
1149
1150   my @transfer_requests;
1151
1152   for my $item (@{ $order->items_sorted }) {
1153     for my $stock (@{ $item->delivery_order_stock_entries }) {
1154       my $transfer = SL::DB::Inventory->new_from($stock);
1155       $transfer->trans_type($trans_type);
1156       $transfer->oe_id($order->id);
1157       $transfer->qty($transfer->qty * -1) if $inout eq 'out';
1158       $transfer->qty($transfer->qty * 1) if $inout eq 'in';
1159       $transfer->comment(t8("Default transfer delivery order")) if $default_transfer;
1160
1161       push @transfer_requests, $transfer if defined $transfer->qty && $transfer->qty != 0;
1162     };
1163   }
1164
1165   if (!@transfer_requests) {
1166     return $self->js->flash("error", t8("No stock to transfer"))->render;
1167   }
1168
1169   if ($inout eq 'out') { # check stock for enough qty
1170     my @missing_qtys = SL::Helper::Inventory::check_stock_out_transfer_requests(
1171       transfer_requests => \@transfer_requests,
1172       default_transfer  => $default_transfer,
1173     );
1174
1175     if (scalar @missing_qtys) {
1176       my $error = t8('The stock is to low: #1.',
1177         join(". ", map {
1178               $_->{chargenumber} && $_->{bestbefore}
1179             ? t8(
1180                 "For #1, #2 #3 are missing of batch with chargenumber #4 and bestbefore date of #5 in bin #6",
1181                 $_->{part}->displayable_name,
1182                 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1183                 $_->{part}->unit,
1184                 $_->{chargenumber},
1185                 DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
1186                 $_->{bin}->full_description,
1187               )
1188             : $_->{chargenumber}
1189             ? t8(
1190                 "For #1, #2 #3 are missing of batch with chargenumber #4 in bin #5",
1191                 $_->{part}->displayable_name,
1192                 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1193                 $_->{part}->unit,
1194                 $_->{chargenumber},
1195                 $_->{bin}->full_description,
1196               )
1197             : $_->{bestbefore}
1198             ? t8(
1199                 "For #1, #2 #3 are missing with a bestbefore date of #4 in bin #5",
1200                 $_->{part}->displayable_name,
1201                 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1202                 $_->{part}->unit,
1203                 DateTime->from_ymdhms($_->{bestbefore})->to_kivitendo,
1204                 $_->{bin}->full_description,
1205               )
1206             : t8(
1207                 "For #1, #2 #3 are missing in bin #4",
1208                 $_->{part}->displayable_name,
1209                 $::form->format_amount(\%::myconfig, $_->{missing_qty}),
1210                 $_->{part}->unit,
1211                 $_->{bin}->full_description,
1212               )
1213             ;
1214           } @missing_qtys
1215         )
1216       );
1217       return $self->js->flash("error", $error)->render;
1218     }
1219   }
1220
1221   SL::DB->client->with_transaction(sub {
1222
1223     $_->save for @transfer_requests;
1224     $self->order->update_attributes(delivered => 1);
1225   });
1226   # update qty and stock info
1227   foreach my $item (@{$self->order->items}) {
1228     $self->order->prepare_stock_info($item);
1229     my $stock_info_yaml = $item->{stock_info};
1230     my $item_position = $item->position;
1231     my $stock_qty = $self->calculate_stock_in_out($item);
1232     my $unit = $item->unit;
1233     $self->js->text("[data-position=$item_position] .data-stock-qty", "$stock_qty $unit");
1234     my $selector = "[data-position=$item_position] .data-stock-info";
1235     $self->js->val($selector, $stock_info_yaml);
1236   }
1237
1238   $self->js
1239     ->flash("info", t8("Stock transfered"))
1240     ->run('kivi.ActionBar.setDisabled', '#save_action',
1241           t8('This record has already been delivered.'))
1242     ->run('kivi.ActionBar.setDisabled', '#save_and_close',
1243           t8('This record has already been delivered.'))
1244     ->run('kivi.ActionBar.setDisabled', '#transfer_out_action',
1245           t8('The parts for this order have already been transferred'))
1246     ->run('kivi.ActionBar.setDisabled', '#transfer_out_default_action',
1247           t8('The parts for this order have already been transferred'))
1248     ->run('kivi.ActionBar.setDisabled', '#transfer_in_action',
1249           t8('The parts for this order have already been transferred'))
1250     ->run('kivi.ActionBar.setDisabled', '#transfer_in_default_action',
1251           t8('The parts for this order have already been transferred'))
1252     ->run('kivi.ActionBar.setDisabled', '#delete_action',
1253           t8('The parts for this order have already been transferred'))
1254     ->run('kivi.ActionBar.setEnabled', '#undo_transfer_action',
1255           t8('The parts for this order have already been transferred'))
1256     ->html('#data-status-line', delivery_order_status_line($self->order))
1257     ->render;
1258 }
1259
1260 sub action_transfer_stock_default {
1261   my ($self) = @_;
1262   my $delivery_order = $self->order;
1263   my @items = @{$delivery_order->items_sorted};
1264
1265   # get default bin if set in config
1266   my ($default_warehouse_id, $default_bin_id);
1267   if ($::instance_conf->get_transfer_default_use_master_default_bin) {
1268     $default_warehouse_id = $::instance_conf->get_warehouse_id;
1269     $default_bin_id       = $::instance_conf->get_bin_id;
1270   }
1271
1272   my @transfer_requests = ();
1273   my %parts_qty = ();
1274   my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
1275   foreach my $item (@items) {
1276     my $part = $item->part;
1277     my $base_unit_factor = $units_by_name{$part->unit}->factor || 1;
1278     my $item_unit_factor = $units_by_name{$item->unit}->factor || 1;
1279     my $qty = $item->qty * $item_unit_factor / $base_unit_factor;
1280     return $self->js->flash('error', t8('Cannot transfer negative entries.'))->render() if $qty < 0;
1281     $qty = 0 if (!$::instance_conf->get_transfer_default_services && $part->is_service);
1282
1283     $parts_qty{$part->id} += $qty if $qty;
1284     push @transfer_requests, {
1285       'warehouse_id'        => $part->warehouse_id || $default_warehouse_id,
1286       'bin_id'              => $part->bin_id       || $default_bin_id,
1287       'unit'                => $item->unit,
1288       'qty'                 => $qty,
1289       # added in check transfer_request out direction if possible
1290       'chargenumber'        => undef, # $item->serialnumber, # Is not used in delivery order
1291       'bestbefore'          => undef, # $item->bestbefore,   # Is not used in delivery order
1292     }
1293   }
1294
1295   # check transfer_requests are correctly
1296   my %parts_errors = (); # missing_bin, missing_qty, multiple_options
1297   my $grouped_qty_query = qq|
1298     SELECT SUM(qty) as qty, chargenumber, bestbefore
1299     FROM inventory
1300     WHERE parts_id = ? AND bin_id = ?
1301     GROUP BY chargenumber, bestbefore
1302   |;
1303   my $dbh = $self->order->dbh;
1304   my $in_out_direction = $delivery_order->type_data->properties('transfer');
1305   for my $idx (0 .. scalar @transfer_requests - 1) {
1306     my $transfer_request = $transfer_requests[$idx];
1307     next unless $transfer_request->{qty}; # empty request
1308     my $item = $items[$idx];
1309     my $part_id = $item->parts_id;
1310     my $bin_id  = $transfer_request->{bin_id};
1311     $parts_errors{$part_id}{missing_bin} = 1 unless $bin_id;
1312     next                                     unless $bin_id;
1313     if ($in_out_direction eq 'out') {
1314       my @grouped_qty = selectall_hashref_query(
1315         $::form, $dbh, $grouped_qty_query, $part_id, $bin_id);
1316
1317       if (1 < scalar grep {$_->{qty} != 0} @grouped_qty) {
1318         $parts_errors{$part_id}{multiple_options} = 1;
1319       }
1320       my $max_qty = sum0(map {$_->{qty}} @grouped_qty);
1321       if ($max_qty < $parts_qty{$part_id}) {
1322         $parts_errors{$part_id}{missing_qty} = $parts_qty{$part_id} - $max_qty;
1323         $parts_errors{$part_id}{bin_id}      = $bin_id;
1324       }
1325
1326       next if $parts_errors{$part_id};
1327       # find correct chargenumber and bestbefore
1328       my $stock_info = first {$_->{qty} >= $transfer_request->{qty}} @grouped_qty;
1329       $transfer_request->{chargenumber} = $stock_info->{chargenumber};
1330       $transfer_request->{bestbefore}   = $stock_info->{bestbefore};
1331     }
1332   }
1333
1334   # auslagern soll immer gehen, auch wenn nicht genügend auf lager ist.
1335   # der lagerplatz ist hier extra konfigurierbar, bspw. Lager-Korrektur mit
1336   # Lagerplatz Lagerplatz-Korrektur
1337   my $default_warehouse_id_ignore_onhand = $::instance_conf->get_warehouse_id_ignore_onhand;
1338   my $default_bin_id_ignore_onhand       = $::instance_conf->get_bin_id_ignore_onhand;
1339   if ($::instance_conf->get_transfer_default_ignore_onhand && $default_bin_id_ignore_onhand) {
1340     foreach my $part_id (keys %parts_errors) {
1341       # entsprechende defaults holen
1342       # falls chargenumber, bestbefore oder anzahl nicht stimmt, auf automatischen
1343       # lagerplatz wegbuchen!
1344       for my $idx (0 .. scalar @transfer_requests - 1) {
1345         my $transfer_request = $transfer_requests[$idx];
1346         next unless $transfer_request->{qty}; # empty request
1347
1348         if ($items[$idx]->parts_id eq $part_id){
1349           $transfer_request->{bin_id}        = $default_bin_id_ignore_onhand;
1350           $transfer_request->{warehouse_id}  = $default_warehouse_id_ignore_onhand;
1351         }
1352       }
1353       delete %parts_errors{$part_id};
1354     }
1355   }
1356
1357   # render errors
1358   if (scalar keys %parts_errors) {
1359     my @multiple_options = ();
1360     foreach my $part_id (keys %parts_errors) {
1361       my $part = SL::DB::Part->new(id => $part_id)->load();
1362       if ($parts_errors{$part_id}{missing_bin}){
1363         $self->js->error(t8('No standard bin set for #1.', $part->displayable_name));
1364       }
1365       if ($parts_errors{$part_id}{missing_qty}) {
1366         my $bin = SL::DB::Manager::Bin->find_by(
1367           id => $parts_errors{$part_id}{bin_id}
1368         );
1369         $self->js->error(
1370           t8('There are #1 of "#2" missing from the bin #3 for transfer.',
1371             $parts_errors{$part_id}{missing_qty}, $part->displayable_name, $bin->full_description));
1372       }
1373       if ($parts_errors{$part_id}{multiple_options}){
1374         push @multiple_options, $part;
1375       }
1376     }
1377     if (scalar @multiple_options) {
1378         $self->js->error(t8(
1379             "There are parts with multiple chargenumbers or bestbefore dates set. This can't be decided automatically. Pleas transfer this delivery order manually. Can't decided for #1.",
1380             join ", ", map {$_->displayable_name} @multiple_options)
1381         );
1382     }
1383     return $self->js->render();
1384   }
1385
1386   # assign each delivery_order_item it's stock
1387   for my $idx (0 .. scalar @transfer_requests - 1) {
1388     my %transfer_request = %{$transfer_requests[$idx]};
1389     next unless $transfer_request{qty}; # empty request
1390
1391     my $item = $items[$idx];
1392     my @stocks = (SL::DB::DeliveryOrderItemsStock->new(%transfer_request));
1393     $item->delivery_order_stock_entries(@stocks);
1394   }
1395
1396   my $default_transfer = 1;
1397   $self->action_transfer_stock($default_transfer);
1398 }
1399
1400 sub action_undo_transfers {
1401   my ( $self ) = @_;
1402
1403   SL::DB->client->with_transaction(sub {
1404     foreach my $item (@{$self->order->orderitems}) {
1405       foreach my $inv_item (@{ $item->delivery_order_stock_entries}) {
1406         $inv_item->inventory->delete;
1407         $inv_item->delete;
1408       }
1409     }
1410     $self->order->update_attributes(delivered => 0);
1411     $self->order->update_attributes(closed => 0);
1412   });
1413
1414   flash_later('info', t8("Transfer undone"));
1415   my @redirect_params = (
1416     action => 'edit',
1417     type   => $self->type,
1418     id     => $self->order->id,
1419   );
1420
1421   $self->redirect_to(@redirect_params);
1422 }
1423
1424 sub js_load_second_row {
1425   my ($self, $item, $item_id, $do_parse) = @_;
1426
1427   if ($do_parse) {
1428     # Parse values from form (they are formated while rendering (template)).
1429     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1430     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1431     foreach my $var (@{ $item->cvars_by_config }) {
1432       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1433     }
1434     $item->parse_custom_variable_values;
1435   }
1436
1437   my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1438
1439   $self->js
1440     ->html('#second_row_' . $item_id, $row_as_html)
1441     ->data('#second_row_' . $item_id, 'loaded', 1);
1442 }
1443
1444 sub js_redisplay_line_values {
1445   my ($self) = @_;
1446
1447   my $is_sales = $self->order->is_sales;
1448
1449   # sales orders with margins
1450   my @data;
1451   if ($is_sales) {
1452     @data = map {
1453       [
1454        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
1455        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
1456        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1457       ]} @{ $self->order->items_sorted };
1458   } else {
1459     @data = map {
1460       [
1461        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
1462       ]} @{ $self->order->items_sorted };
1463   }
1464
1465   $self->js
1466     ->run('kivi.DeliveryOrder.redisplay_line_values', $is_sales, \@data);
1467 }
1468
1469 sub js_redisplay_cvpartnumbers {
1470   my ($self) = @_;
1471
1472   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1473
1474   my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1475
1476   $self->js
1477     ->run('kivi.DeliveryOrder.redisplay_cvpartnumbers', \@data);
1478 }
1479
1480 sub js_reset_order_and_item_ids_after_save {
1481   my ($self) = @_;
1482
1483   $self->js
1484     ->val('#id', $self->order->id)
1485     ->val('#converted_from_record_type_ref', '')
1486     ->val('#converted_from_record_id',  '')
1487     ->val('#order_' . $self->nr_key(), $self->order->number);
1488
1489   my $idx = 0;
1490   foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1491     next if !$self->order->items_sorted->[$idx]->id;
1492     next if $form_item_id !~ m{^new};
1493     $self->js
1494       ->val (
1495         '[name="orderitem_ids[+]"][value="' . $form_item_id . '"]',
1496         $self->order->items_sorted->[$idx]->id)
1497       ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1498       ->attr('#item_' . $form_item_id, "id",
1499         'item_' . $self->order->items_sorted->[$idx]->id);
1500   } continue {
1501     $idx++;
1502   }
1503   $self->js->val('[name="converted_from_record_item_type_refs[+]"]', '');
1504   $self->js->val('[name="converted_from_record_item_ids[+]"]', '');
1505 }
1506
1507 #
1508 # helpers
1509 #
1510
1511 sub init_type {
1512   my ($self) = @_;
1513
1514   my $type = $self->order->record_type;
1515   if (none { $type eq $_ } @{$self->valid_types}) {
1516     die "Not a valid type for delivery order";
1517   }
1518
1519   $self->type($type);
1520 }
1521
1522 sub init_cv {
1523   my ($self) = @_;
1524
1525   return $self->type_data->properties("customervendor");
1526 }
1527
1528 sub init_search_cvpartnumber {
1529   my ($self) = @_;
1530
1531   my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1532   my $search_cvpartnumber;
1533   $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1534   $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
1535
1536   return $search_cvpartnumber;
1537 }
1538
1539 sub init_show_update_button {
1540   my ($self) = @_;
1541
1542   !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1543 }
1544
1545 sub init_p {
1546   SL::Presenter->get;
1547 }
1548
1549 sub init_order {
1550   $_[0]->make_order;
1551 }
1552
1553 sub init_part_picker_classification_ids {
1554   my ($self)    = @_;
1555
1556   return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
1557     where => $self->type_data->part_classification_query
1558     ) } ];
1559 }
1560
1561 sub check_auth {
1562   my ($self) = @_;
1563
1564   $::auth->assert($self->type_data->rights('view') || 'DOES_NOT_EXIST');
1565 }
1566
1567 sub check_auth_for_edit {
1568   my ($self) = @_;
1569
1570   $::auth->assert($self->type_data->rights('edit') || 'DOES_NOT_EXIST');
1571 }
1572
1573 # build the selection box for contacts
1574 #
1575 # Needed, if customer/vendor changed.
1576 sub build_contact_select {
1577   my ($self) = @_;
1578
1579   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1580     value_key  => 'cp_id',
1581     title_key  => 'full_name_dep',
1582     default    => $self->order->cp_id,
1583     with_empty => 1,
1584     style      => 'width: 300px',
1585   );
1586 }
1587
1588 # build the selection box for shiptos
1589 #
1590 # Needed, if customer/vendor changed.
1591 sub build_shipto_select {
1592   my ($self) = @_;
1593
1594   select_tag('order.shipto_id',
1595              [ {
1596                  displayable_id => t8("No/individual shipping address"),
1597                  shipto_id => ''
1598                },
1599                $self->order->{$self->cv}->shipto ],
1600              value_key  => 'shipto_id',
1601              title_key  => 'displayable_id',
1602              default    => $self->order->shipto_id,
1603              with_empty => 0,
1604              style      => 'width: 300px',
1605   );
1606 }
1607
1608 # build the inputs for the cusom shipto dialog
1609 #
1610 # Needed, if customer/vendor changed.
1611 sub build_shipto_inputs {
1612   my ($self) = @_;
1613
1614   my $content = $self->p->render('common/_ship_to_dialog',
1615                                  vc_obj      => $self->order->customervendor,
1616                                  cs_obj      => $self->order->custom_shipto,
1617                                  cvars       => $self->order->custom_shipto->cvars_by_config,
1618                                  id_selector => '#order_shipto_id');
1619
1620   div_tag($content, id => 'shipto_inputs');
1621 }
1622
1623 # render the info line for business
1624 #
1625 # Needed, if customer/vendor changed.
1626 sub build_business_info_row
1627 {
1628   $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
1629 }
1630
1631
1632 sub load_order {
1633   my ($self) = @_;
1634
1635   return if !$::form->{id};
1636
1637   $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
1638
1639   $self->reinit_after_new_order();
1640
1641   return $self->order;
1642 }
1643
1644 # load or create a new order object
1645 #
1646 # And assign changes from the form to this object.
1647 # If the order is loaded from db, check if items are deleted in the form,
1648 # remove them form the object and collect them for removing from db on saving.
1649 # Then create/update items from form (via make_item) and add them.
1650 sub make_order {
1651   my ($self) = @_;
1652
1653   # add_items adds items to an order with no items for saving, but they cannot
1654   # be retrieved via items until the order is saved. Adding empty items to new
1655   # order here solves this problem.
1656   my $order;
1657   if ($::form->{id}) {
1658     $order = SL::DB::DeliveryOrder->new(
1659       id => $::form->{id}
1660     )->load(
1661       with => [
1662         'orderitems',
1663         'orderitems.part',
1664       ]
1665     );
1666   } else {
1667     $order = SL::DB::DeliveryOrder->new(
1668       orderitems  => [],
1669       currency_id => $::instance_conf->get_currency_id(),
1670       record_type => $::form->{type}
1671     );
1672     $order = SL::Model::Record->update_after_new($order);
1673   }
1674
1675   my $cv_id_method = $order->type_data->properties('customervendor'). '_id';
1676   if (!$::form->{id} && $::form->{$cv_id_method}) {
1677     $order->$cv_id_method($::form->{$cv_id_method});
1678     $order = SL::Model::Record->update_after_customer_vendor_change($order);
1679   }
1680
1681   # don't assign hashes as objects
1682   my $form_orderitems = delete $::form->{order}->{orderitems};
1683
1684   $order->assign_attributes(%{$::form->{order}});
1685
1686   # restore form values
1687   $::form->{order}->{orderitems} = $form_orderitems;
1688
1689   $self->setup_custom_shipto_from_form($order, $::form);
1690
1691   # remove deleted items
1692   $self->item_ids_to_delete([]);
1693   foreach my $idx (reverse 0..$#{$order->orderitems}) {
1694     my $item = $order->orderitems->[$idx];
1695     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1696       splice @{$order->orderitems}, $idx, 1;
1697       push @{$self->item_ids_to_delete}, $item->id;
1698     }
1699   }
1700
1701   my @items;
1702   my $pos = 1;
1703   foreach my $form_attr (@{$form_orderitems}) {
1704     my $item = make_item($order, $form_attr);
1705     $item->position($pos);
1706     push @items, $item;
1707     $pos++;
1708   }
1709
1710   $order->add_items(grep {!$_->id} @items);
1711
1712   return $order;
1713 }
1714
1715 # create or update items from form
1716 #
1717 # Make item objects from form values. For items already existing read from db.
1718 # Create a new item else. And assign attributes.
1719 sub make_item {
1720   my ($record, $attr) = @_;
1721
1722   my $item;
1723   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1724
1725   my $is_new = !$item;
1726
1727   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1728   # they cannot be retrieved via custom_variables until the order/orderitem is
1729   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1730   $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
1731
1732   # handle stock info
1733   if (my $stock_info = delete $attr->{stock_info}) {
1734     my %existing = map { $_->id => $_ } $item->delivery_order_stock_entries;
1735     my @save;
1736
1737     for my $line (@{ DO->unpack_stock_information(packed => $stock_info) }) {
1738       # lookup existing or make new
1739       my $obj = delete $existing{$line->{delivery_order_items_stock_id}}
1740              // SL::DB::DeliveryOrderItemsStock->new;
1741
1742       # assign attributes
1743       $obj->$_($line->{$_}) for qw(bin_id warehouse_id chargenumber qty unit);
1744       $obj->bestbefore_as_date($line->{bestfbefore})
1745         if $line->{bestbefore} && $::instance_conf->get_show_bestbefore;
1746       push @save, $obj if $obj->qty;
1747     }
1748
1749     $item->delivery_order_stock_entries(@save);
1750   }
1751
1752   $item->assign_attributes(%$attr);
1753
1754   if ($is_new) {
1755     my $texts = get_part_texts($item->part, $record->language_id);
1756     $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
1757     $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
1758     $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1759   }
1760
1761   return $item;
1762 }
1763
1764 # create a new item
1765 #
1766 # This is used to add one item
1767 sub new_item {
1768   my ($record, $attr) = @_;
1769
1770   my $item = SL::DB::DeliveryOrderItem->new;
1771
1772   # Remove attributes where the user left or set the inputs empty.
1773   # So these attributes will be undefined and we can distinguish them
1774   # from zero later on.
1775   for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1776     delete $attr->{$_} if $attr->{$_} eq '';
1777   }
1778
1779   $item->assign_attributes(%$attr);
1780
1781   my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1782   $item->qty(1.0)          if !$item->qty;
1783   $item->unit($part->unit) if !$item->unit;
1784
1785   my ($price_src, $discount_src) = SL::Model::Record->get_best_price_and_discount_source($record, $item, ignore_given => 0);
1786
1787   my %new_attr;
1788   $new_attr{part}                   = $part;
1789   $new_attr{description}            = $part->description     if ! $item->description;
1790   $new_attr{qty}                    = 1.0                    if ! $item->qty;
1791   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
1792   $new_attr{sellprice}              = $price_src->price;
1793   $new_attr{discount}               = $discount_src->discount;
1794   $new_attr{active_price_source}    = $price_src;
1795   $new_attr{active_discount_source} = $discount_src;
1796   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
1797   $new_attr{project_id}             = $record->globalproject_id;
1798   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
1799
1800   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1801   # they cannot be retrieved via custom_variables until the order/orderitem is
1802   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1803   $new_attr{custom_variables} = [];
1804
1805   my $texts = get_part_texts(
1806     $part, $record->language_id,
1807     description => $new_attr{description},
1808     longdescription => $new_attr{longdescription}
1809   );
1810
1811   $item->assign_attributes(%new_attr, %{ $texts });
1812
1813   return $item;
1814 }
1815
1816 # setup custom shipto from form
1817 #
1818 # The dialog returns form variables starting with 'shipto' and cvars starting
1819 # with 'shiptocvar_'.
1820 # Mark it to be deleted if a shipto from master data is selected
1821 # (i.e. order has a shipto).
1822 # Else, update or create a new custom shipto. If the fields are empty, it
1823 # will not be saved on save.
1824 sub setup_custom_shipto_from_form {
1825   my ($self, $order, $form) = @_;
1826
1827   if ($order->shipto) {
1828     $self->is_custom_shipto_to_delete(1);
1829   } else {
1830     my $custom_shipto =
1831          $order->custom_shipto
1832       || $order->custom_shipto(
1833            SL::DB::Shipto->new(module => 'DO', custom_variables => [])
1834          );
1835
1836     my $shipto_cvars  = {
1837       map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}}
1838       grep { m{^shiptocvar_} }
1839       keys %$form
1840     };
1841     my $shipto_attrs  = {
1842       map { $_ => delete $form->{$_}}
1843       grep { m{^shipto} }
1844       keys %$form
1845     };
1846
1847     $custom_shipto->assign_attributes(%$shipto_attrs);
1848     $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1849   }
1850 }
1851
1852 # get data for saving, printing, ..., that is not changed in the form
1853 #
1854 # Only cvars for now.
1855 sub get_unalterable_data {
1856   my ($self) = @_;
1857
1858   foreach my $item (@{ $self->order->items }) {
1859     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1860     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1861     foreach my $var (@{ $item->cvars_by_config }) {
1862       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1863     }
1864     $item->parse_custom_variable_values;
1865   }
1866 }
1867
1868 # save the order
1869 #
1870 # And delete items that are deleted in the form.
1871 sub save {
1872   my ($self) = @_;
1873
1874   set_record_link_conversions($self->order,
1875     delete $::form->{RECORD_TYPE_REF()}
1876       => delete $::form->{RECORD_ID()},
1877     delete $::form->{RECORD_ITEM_TYPE_REF()}
1878       => delete $::form->{RECORD_ITEM_ID()},
1879   );
1880
1881   my $items_to_delete  = scalar @{ $self->item_ids_to_delete || [] }
1882                        ? SL::DB::Manager::DeliveryOrderItem->get_all(where => [id => $self->item_ids_to_delete])
1883                        : undef;
1884
1885   SL::Model::Record->save($self->order,
1886     with_validity_token        => {
1887       scope => SL::DB::ValidityToken::SCOPE_DELIVERY_ORDER_SAVE(),
1888       token => $::form->{form_validity_token}
1889     },
1890     delete_custom_shipto       => $self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty),
1891     items_to_delete            => $items_to_delete,
1892   );
1893
1894   if ($::form->{email_journal_id}) {
1895     my $email_journal = SL::DB::EmailJournal->new(
1896       id => delete $::form->{email_journal_id}
1897     )->load;
1898     $email_journal->link_to_record_with_attachment(
1899       $self->order,
1900       delete $::form->{email_attachment_id}
1901     );
1902   }
1903
1904   delete $::form->{form_validity_token};
1905 }
1906
1907 sub reinit_after_new_order {
1908   my ($self) = @_;
1909
1910   # change form type
1911   $::form->{type} = $self->order->type;
1912   $self->type($self->init_type);
1913   $self->type_data($self->init_type_data);
1914   $self->cv($self->init_cv);
1915   $self->check_auth;
1916
1917   $self->setup_custom_shipto_from_form($self->order, $::form);
1918
1919   foreach my $item (@{$self->order->items_sorted}) {
1920     # set item ids to new fake id, to identify them as new items
1921     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1922
1923     # trigger rendering values for second row as hidden, because they
1924     # are loaded only on demand. So we need to keep the values from the
1925     # source.
1926     $item->{render_second_row} = 1;
1927   }
1928
1929   $self->order->prepare_stock_info($_) for $self->order->items;
1930   $self->get_unalterable_data();
1931 }
1932
1933
1934 sub pre_render {
1935   my ($self) = @_;
1936
1937   $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
1938   $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
1939   $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
1940   $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->order->language_id ] ] );
1941   $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1942                                                                                               deleted => 0 ] ],
1943                                                                            sort_by => 'name');
1944   $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1945                                                                                               deleted => 0 ] ],
1946                                                                            sort_by => 'name');
1947   $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1948                                                                                                         obsolete => 0 ] ]);
1949   $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1950   $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
1951   $self->{order_probabilities}        = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1952   $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1953
1954   my $print_form = Form->new('');
1955   $print_form->{type}        = $self->type;
1956   $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
1957   $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
1958     form => $print_form,
1959     options => {dialog_name_prefix => 'print_options.',
1960                 show_headers       => 1,
1961                 no_queue           => 1,
1962                 no_postscript      => 1,
1963                 no_opendocument    => 0,
1964                 no_html            => 1},
1965   );
1966
1967   foreach my $item (@{$self->order->orderitems}) {
1968     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1969     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1970     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1971   }
1972
1973   if ($self->order->${\ $self->type_data->properties("nr_key") } && $::instance_conf->get_webdav) {
1974     my $webdav = SL::Webdav->new(
1975       type     => $self->type,
1976       number   => $self->order->number,
1977     );
1978     my @all_objects = $webdav->get_all_objects;
1979     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1980                                                     type => t8('File'),
1981                                                     link => File::Spec->catfile($_->full_filedescriptor),
1982                                                 } } @all_objects;
1983   }
1984
1985   $self->{template_args}{in_out}                                 = $self->type_data->properties("transfer");
1986   $self->{template_args}{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
1987
1988   $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1989
1990   $::request->{layout}->use_javascript("${_}.js") for qw(
1991     kivi.SalesPurchase kivi.DeliveryOrder kivi.File calculate_qty kivi.Validator
1992     follow_up show_history
1993     );
1994   $self->setup_edit_action_bar;
1995 }
1996
1997 sub setup_edit_action_bar {
1998   my ($self, %params) = @_;
1999
2000   my $deletion_allowed = $self->type_data->show_menu("delete");
2001   my $may_edit_create  = $::auth->assert(
2002     $self->type_data->rights('edit') || 'DOES_NOT_EXIST', 1
2003   );
2004
2005   my $confirmation_on_workflow = $self->order->delivered ? undef
2006     : ( $self->order->is_sales && $::instance_conf->get_sales_delivery_order_check_stocked)    ? t8('This record has not been stocked out. Proceed?')
2007     : (!$self->order->is_sales && $::instance_conf->get_purchase_delivery_order_check_stocked) ? t8('This record has not been stocked in. Proceed?')
2008     : undef;
2009
2010   for my $bar ($::request->layout->get('actionbar')) {
2011     $bar->add(
2012       combobox => [
2013         action => [
2014           t8('Save'),
2015           id       => 'save_action',
2016           call     => [ 'kivi.DeliveryOrder.save', {
2017               action             => 'save',
2018               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2019               warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
2020             }],
2021           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2022                     : $self->order->delivered ? t8('This record has already been delivered.')
2023                     :                           undef,
2024         ],
2025         action => [
2026           t8('Save and Close'),
2027           id       => 'save_and_close',
2028           call     => [ 'kivi.DeliveryOrder.save', {
2029               action             => 'save',
2030               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2031               warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
2032               form_params        => [
2033                 { name => 'back_to_caller', value => 1 },
2034               ],
2035             }],
2036           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2037                     : $self->order->delivered ? t8('This record has already been delivered.')
2038                     :                           undef,
2039         ],
2040         action => [
2041           t8('Mark as closed'),
2042           id       => 'close_order',
2043           call     => [ 'kivi.DeliveryOrder.close_order' ],
2044           confirm  => t8('This will remove the delivery order from showing as open even if contents are not delivered. Proceed?'),
2045           disabled => !$may_edit_create    ? t8('You do not have the permissions to access this function.')
2046                     : !$self->order->id    ? t8('This object has not been saved yet.')
2047                     : $self->order->closed ? t8('This record has already been closed.')
2048                     :                        undef,
2049         ],
2050         action => [
2051           t8('Save as new'),
2052           call     => [ 'kivi.DeliveryOrder.save', {
2053               action             => 'save_as_new',
2054               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2055             }],
2056           disabled => !$may_edit_create                        ? t8('You do not have the permissions to access this function.')
2057                     : $self->type eq 'supplier_delivery_order' ? t8('Need a workflow for Supplier Delivery Order')
2058                     : $self->type eq 'rma_delivery_order'      ? t8('Need a workflow for RMA Delivery Order.')
2059                     : !$self->order->id                        ? t8('This object has not been saved yet.')
2060                     :                                            undef,
2061         ],
2062       ], # end of combobox "Save"
2063
2064       combobox => [
2065         action => [
2066           t8('Workflow'),
2067         ],
2068         action => [
2069           t8('Create Invoice'),
2070           call     => [ 'kivi.DeliveryOrder.save', {
2071               action             => 'workflow_invoice',
2072               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2073             }],
2074           only_if  => $self->type_data->show_menu("workflow_invoice"),
2075           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2076                     : !$self->order->id ? t8('This object has not been saved yet.')
2077                     : undef,
2078           confirm  =>   $confirmation_on_workflow,
2079         ],
2080         action => [
2081           t8('Create Reclamation'),
2082           call      => [ 'kivi.DeliveryOrder.save', {
2083               action             => 'workflow_new_record',
2084               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2085               form_params        => [
2086                 { name => 'to_type',
2087                   value => $self->order->is_sales ? SALES_RECLAMATION_TYPE()
2088                                                   : PURCHASE_RECLAMATION_TYPE() },
2089               ],
2090             }],
2091           only_if  => $self->type_data->show_menu('workflow_reclamation'),
2092           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2093                     : !$self->order->id ? t8('This object has not been saved yet.')
2094                     : undef,
2095           confirm  => $confirmation_on_workflow,
2096         ],
2097
2098       ], # end of combobox "Workflow"
2099
2100       combobox => [
2101         action => [
2102           t8('Export'),
2103         ],
2104         action => [
2105           t8('Save and preview PDF'),
2106            call    => [ 'kivi.DeliveryOrder.save', {
2107                action             => 'preview_pdf',
2108                warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2109                warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
2110              }],
2111           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2112         ],
2113         action => [
2114           t8('Save and print'),
2115           call     => [ 'kivi.DeliveryOrder.show_print_options', {
2116               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2117               warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate },
2118           ],
2119           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.') : undef,
2120         ],
2121         action => [
2122           t8('Save and E-mail'),
2123           id       => 'save_and_email_action',
2124           call     => [ 'kivi.DeliveryOrder.save', {
2125               action             => 'save_and_show_email_dialog',
2126               warn_on_duplicates => $::instance_conf->get_order_warn_duplicate_parts,
2127               warn_on_reqdate    => $::instance_conf->get_order_warn_no_deliverydate,
2128             }],
2129           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2130                     : !$self->order->id ? t8('This object has not been saved yet.')
2131                     :                     undef,
2132         ],
2133         action => [
2134           t8('Download attachments of all parts'),
2135           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
2136           disabled => !$may_edit_create ? t8('You do not have the permissions to access this function.')
2137                     : !$self->order->id ? t8('This object has not been saved yet.')
2138                     :                     undef,
2139           only_if  => $::instance_conf->get_doc_storage,
2140         ],
2141       ], # end of combobox "Export"
2142
2143       action => [
2144         t8('Delete'),
2145         id       => 'delete_action',
2146         call     => [ 'kivi.DeliveryOrder.delete_order' ],
2147         confirm  => $::locale->text('Do you really want to delete this object?'),
2148         disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2149                   : !$self->order->id       ? t8('This object has not been saved yet.')
2150                   : $self->order->delivered ? t8('The parts for this order have already been transferred')
2151                   :                           undef,
2152         only_if  => $self->type_data->show_menu("delete"),
2153       ],
2154
2155       combobox => [
2156         action => [
2157           t8('Transfer out'),
2158           id       => 'transfer_out_action',
2159           call     => [ 'kivi.DeliveryOrder.save', {
2160               action => 'transfer_stock',
2161             }],
2162           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2163                     : !$self->order->id       ? t8('This object has not been saved yet.')
2164                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
2165                     :                           undef,
2166           only_if  => $self->type_data->properties('transfer') eq 'out',
2167           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
2168         ],
2169         action => [
2170           t8('Transfer out via default'),
2171           id       => 'transfer_out_default_action',
2172           call     => [ 'kivi.DeliveryOrder.save', {
2173               action => 'transfer_stock_default',
2174             }],
2175           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2176                     : !$self->order->id       ? t8('This object has not been saved yet.')
2177                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
2178                     :                           undef,
2179           only_if  => $self->type_data->properties('transfer') eq 'out',
2180           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
2181         ],
2182         action => [
2183           t8('Transfer in'),
2184           id       => 'transfer_in_action',
2185           call     => [ 'kivi.DeliveryOrder.save', {
2186               action => 'transfer_stock',
2187             }],
2188           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2189                     : !$self->order->id       ? t8('This object has not been saved yet.')
2190                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
2191                     :                           undef,
2192           only_if  => $self->type_data->properties('transfer') eq 'in',
2193           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
2194         ],
2195         action => [
2196           t8('Transfer in via default'),
2197           id       => 'transfer_in_default_action',
2198           call     => [ 'kivi.DeliveryOrder.save', {
2199               action => 'transfer_stock_default',
2200             }],
2201           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2202                     : !$self->order->id       ? t8('This object has not been saved yet.')
2203                     : $self->order->delivered ? t8('The parts for this order have already been transferred')
2204                     :                           undef,
2205           only_if  => $self->type_data->properties('transfer') eq 'in',
2206           confirm  => t8('Do you really want to transfer the stock and set this order to delivered?'),
2207         ],
2208         action => [
2209           t8('Undo Transfer'),
2210           id       => 'undo_transfer_action',
2211           call     => [ 'kivi.DeliveryOrder.save', {
2212               action => 'undo_transfers',
2213             }],
2214           disabled => !$may_edit_create       ? t8('You do not have the permissions to access this function.')
2215                     : !$self->order->id       ? t8('This object has not been saved yet.')
2216                     : undef,
2217           disabled => !$self->order->delivered,
2218           confirm => t8('Do you really want undo transfers the stock and set this order to undelivered?'),
2219         ],
2220       ],
2221
2222       combobox => [
2223         action => [
2224           t8('more')
2225         ],
2226         action => [
2227           t8('Follow-Up'),
2228           call     => [ 'kivi.DeliveryOrder.follow_up_window' ],
2229           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
2230           only_if  => $::auth->assert('productivity', 1),
2231         ],
2232         action => [
2233           t8('History'),
2234           call     => [ 'set_history_window', $self->order->id, 'id' ],
2235           disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
2236         ],
2237       ], # end of combobox "more"
2238     );
2239   }
2240 }
2241
2242 sub generate_pdf {
2243   my ($order, $pdf_ref, $params) = @_;
2244
2245   my @errors = ();
2246
2247   my $print_form = Form->new('');
2248   $print_form->{type}        = $order->type;
2249   $print_form->{formname}    = $params->{formname} || $order->type;
2250   $print_form->{format}      = $params->{format}   || 'pdf';
2251   $print_form->{media}       = $params->{media}    || 'file';
2252   $print_form->{groupitems}  = $params->{groupitems};
2253   $print_form->{printer_id}  = $params->{printer_id};
2254   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
2255
2256   $order->language($params->{language});
2257   $order->flatten_to_form($print_form, format_amounts => 1);
2258
2259   my $template_ext;
2260   my $template_type;
2261   if ($print_form->{format} =~ /(opendocument|oasis)/i) {
2262     $template_ext  = 'odt';
2263     $template_type = 'OpenDocument';
2264   }
2265
2266   # search for the template
2267   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
2268     name        => $print_form->{formname},
2269     extension   => $template_ext,
2270     email       => $print_form->{media} eq 'email',
2271     language    => $params->{language},
2272     printer_id  => $print_form->{printer_id},
2273   );
2274
2275   if (!defined $template_file) {
2276     push @errors, $::locale->text(
2277       'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.',
2278       join ', ', map { "'$_'"} @template_files
2279     );
2280   }
2281
2282   return @errors if scalar @errors;
2283
2284   $print_form->throw_on_error(sub {
2285     eval {
2286       $print_form->prepare_for_printing;
2287
2288       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
2289         format        => $print_form->{format},
2290         template_type => $template_type,
2291         template      => $template_file,
2292         variables     => $print_form,
2293         variable_content_types => {
2294           longdescription => 'html',
2295           partnotes       => 'html',
2296           notes           => 'html',
2297         },
2298       );
2299       1;
2300     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
2301   });
2302
2303   return @errors;
2304 }
2305
2306 sub get_files_for_email_dialog {
2307   my ($self) = @_;
2308
2309   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
2310
2311   return %files if !$::instance_conf->get_doc_storage;
2312
2313   if ($self->order->id) {
2314     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
2315     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
2316     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
2317     $files{project_files} = [ SL::File->get_all(    object_id => $self->order->globalproject_id, object_type => 'project',         file_type => 'attachment') ];
2318   }
2319
2320   my @parts =
2321     uniq_by { $_->{id} }
2322     map {
2323       +{ id         => $_->part->id,
2324          partnumber => $_->part->partnumber }
2325     } @{$self->order->items_sorted};
2326
2327   foreach my $part (@parts) {
2328     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
2329     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
2330   }
2331
2332   foreach my $key (keys %files) {
2333     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
2334   }
2335
2336   return %files;
2337 }
2338
2339 sub get_title_for {
2340   my ($self, $action) = @_;
2341
2342   return '' if none { lc($action)} qw(add edit);
2343   return $self->type_data->text($action);
2344 }
2345
2346 sub get_item_cvpartnumber {
2347   my ($self, $item) = @_;
2348
2349   return if !$self->search_cvpartnumber;
2350   return if !$self->order->customervendor;
2351
2352   if ($self->cv eq 'vendor') {
2353     my @mms =
2354       grep { $_->make eq $self->order->customervendor->id }
2355       @{$item->part->makemodels};
2356     $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
2357   } elsif ($self->cv eq 'customer') {
2358     my @cps =
2359       grep { $_->customer_id eq $self->order->customervendor->id }
2360       @{$item->part->customerprices};
2361     $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
2362   }
2363 }
2364
2365 sub get_part_texts {
2366   my ($part_or_id, $language_or_id, %defaults) = @_;
2367
2368   my $part        = ref($part_or_id)     ? $part_or_id         : SL::DB::Part->load_cached($part_or_id);
2369   my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
2370   my $texts       = {
2371     description     => $defaults{description}     // $part->description,
2372     longdescription => $defaults{longdescription} // $part->notes,
2373   };
2374
2375   return $texts unless $language_id;
2376
2377   my $translation = SL::DB::Manager::Translation->get_first(
2378     where => [
2379       parts_id    => $part->id,
2380       language_id => $language_id,
2381     ]);
2382
2383   $texts->{description}     = $translation->translation     if $translation && $translation->translation;
2384   $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2385
2386   return $texts;
2387 }
2388
2389 sub nr_key {
2390   return $_[0]->type_data->properties("nr_key");
2391 }
2392
2393 sub save_history {
2394   my ($self, $addition) = @_;
2395
2396   my $number_type = $self->nr_key;
2397   my $snumbers    = $number_type . '_' . $self->order->$number_type;
2398
2399   SL::DB::History->new(
2400     trans_id    => $self->order->id,
2401     employee_id => SL::DB::Manager::Employee->current->id,
2402     what_done   => $self->order->type,
2403     snumbers    => $snumbers,
2404     addition    => $addition,
2405   )->save;
2406 }
2407
2408 sub store_pdf_to_webdav_and_filemanagement {
2409   my($order, $content, $filename, $variant) = @_;
2410
2411   my @errors;
2412
2413   # copy file to webdav folder
2414   if ($order->number && $::instance_conf->get_webdav_documents) {
2415     my $webdav = SL::Webdav->new(
2416       type     => $order->type,
2417       number   => $order->number,
2418     );
2419     my $webdav_file = SL::Webdav::File->new(
2420       webdav   => $webdav,
2421       filename => $filename,
2422     );
2423     eval {
2424       $webdav_file->store(data => \$content);
2425       1;
2426     } or do {
2427       push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
2428     };
2429   }
2430   if ($order->id && $::instance_conf->get_doc_storage) {
2431     eval {
2432       SL::File->save(object_id     => $order->id,
2433                      object_type   => $order->type,
2434                      mime_type     => 'application/pdf',
2435                      source        => 'created',
2436                      file_type     => 'document',
2437                      file_name     => $filename,
2438                      file_contents => $content,
2439                      print_variant => $variant);
2440       1;
2441     } or do {
2442       push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2443     };
2444   }
2445
2446   return @errors;
2447 }
2448
2449 sub calculate_stock_in_out_from_stock_info {
2450   my ($self, $unit, $stock_info) = @_;
2451
2452   return "" if !$unit;
2453
2454   my %units_by_name = map { $_->name => $_ } @{ SL::DB::Manager::Unit->get_all };
2455
2456   my $sum      = sum0 map {
2457     $units_by_name{$_->{unit}}->convert_to($_->{qty}, $units_by_name{$unit})
2458   } @$stock_info;
2459
2460   my $content  = _format_number($sum, 2) . ' ' . $unit;
2461
2462   return $content;
2463 }
2464
2465 sub calculate_stock_in_out {
2466   my ($self, $item, $stock_info) = @_;
2467
2468   return "" if !$item->part || !$item->part->unit || !$item->unit;
2469
2470   my $sum      = sum0 map {
2471     $_->unit_obj->convert_to($_->qty, $item->unit_obj)
2472   } $item->delivery_order_stock_entries;
2473
2474   my $content  = _format_number($sum, 2);
2475
2476   return $content;
2477 }
2478
2479 sub init_type_data {
2480   my ($self) = @_;
2481   SL::DB::Helper::TypeDataProxy->new('SL::DB::DeliveryOrder', $self->order->record_type);
2482 }
2483
2484 sub init_valid_types {
2485   $_[0]->type_data->valid_types;
2486 }
2487
2488 1;
2489
2490 __END__
2491
2492 =encoding utf-8
2493
2494 =head1 NAME
2495
2496 SL::Controller::Order - controller for orders
2497
2498 =head1 SYNOPSIS
2499
2500 This is a new form to enter orders, completely rewritten with the use
2501 of controller and java script techniques.
2502
2503 The aim is to provide the user a better experience and a faster workflow. Also
2504 the code should be more readable, more reliable and better to maintain.
2505
2506 =head2 Key Features
2507
2508 =over 4
2509
2510 =item *
2511
2512 One input row, so that input happens every time at the same place.
2513
2514 =item *
2515
2516 Use of pickers where possible.
2517
2518 =item *
2519
2520 Possibility to enter more than one item at once.
2521
2522 =item *
2523
2524 Item list in a scrollable area, so that the workflow buttons stay at
2525 the bottom.
2526
2527 =item *
2528
2529 Reordering item rows with drag and drop is possible. Sorting item rows is
2530 possible (by partnumber, description, qty, sellprice and discount for now).
2531
2532 =item *
2533
2534 No C<update> is necessary. All entries and calculations are managed
2535 with ajax-calls and the page only reloads on C<save>.
2536
2537 =item *
2538
2539 User can see changes immediately, because of the use of java script
2540 and ajax.
2541
2542 =back
2543
2544 =head1 CODE
2545
2546 =head2 Layout
2547
2548 =over 4
2549
2550 =item * C<SL/Controller/Order.pm>
2551
2552 the controller
2553
2554 =item * C<template/webpages/delivery_order/form.html>
2555
2556 main form
2557
2558 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
2559
2560 Main tab for basic_data.
2561
2562 This is the only tab here for now. "linked records" and "webdav" tabs are
2563 reused from generic code.
2564
2565 =over 4
2566
2567 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
2568
2569 For displaying information on business type
2570
2571 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
2572
2573 The input line for items
2574
2575 =item * C<template/webpages/delivery_order/tabs/_row.html>
2576
2577 One row for already entered items
2578
2579 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
2580
2581 Displaying tax information
2582
2583 =back
2584
2585 =item * C<js/kivi.DeliveryOrder.js>
2586
2587 java script functions
2588
2589 =back
2590
2591 =head1 TODO
2592
2593 =over 4
2594
2595 =item * testing
2596
2597 =item * price sources: little symbols showing better price / better discount
2598
2599 =item * select units in input row?
2600
2601 =item * check for direct delivery (workflow sales order -> purchase order)
2602
2603 =item * access rights
2604
2605 =item * display weights
2606
2607 =item * mtime check
2608
2609 =item * optional client/user behaviour
2610
2611 (transactions has to be set - department has to be set -
2612  force project if enabled in client config - transport cost reminder)
2613
2614 =back
2615
2616 =head1 KNOWN BUGS AND CAVEATS
2617
2618 =over 4
2619
2620 =item *
2621
2622 Customer discount is not displayed as a valid discount in price source popup
2623 (this might be a bug in price sources)
2624
2625 (I cannot reproduce this (Bernd))
2626
2627 =item *
2628
2629 No indication that <shift>-up/down expands/collapses second row.
2630
2631 =item *
2632
2633 Inline creation of parts is not currently supported
2634
2635 =item *
2636
2637 Table header is not sticky in the scrolling area.
2638
2639 =item *
2640
2641 Sorting does not include C<position>, neither does reordering.
2642
2643 This behavior was implemented intentionally. But we can discuss, which behavior
2644 should be implemented.
2645
2646 =back
2647
2648 =head1 To discuss / Nice to have
2649
2650 =over 4
2651
2652 =item *
2653
2654 How to expand/collapse second row. Now it can be done clicking the icon or
2655 <shift>-up/down.
2656
2657 =item *
2658
2659 Possibility to select PriceSources in input row?
2660
2661 =item *
2662
2663 This controller uses a (changed) copy of the template for the PriceSource
2664 dialog. Maybe there could be used one code source.
2665
2666 =item *
2667
2668 Rounding-differences between this controller (PriceTaxCalculator) and the old
2669 form. This is not only a problem here, but also in all parts using the PTC.
2670 There exists a ticket and a patch. This patch should be testet.
2671
2672 =item *
2673
2674 An indicator, if the actual inputs are saved (like in an
2675 editor or on text processing application).
2676
2677 =item *
2678
2679 A warning when leaving the page without saving unchanged inputs.
2680
2681
2682 =back
2683
2684 =head1 AUTHOR
2685
2686 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
2687
2688 =cut