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