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