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