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