Auftrags-Controller: Save as new
[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', $::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_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_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_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         ],
1429         action => [
1430           t8('Purchase Order'),
1431           submit  => [ '#order_form', { action => "Order/purchase_order" } ],
1432           only_if   => (any { $self->type eq $_ } (_request_quotation_type())),
1433         ],
1434       ], # end of combobox "Workflow"
1435
1436       combobox => [
1437         action => [
1438           t8('Export'),
1439         ],
1440         action => [
1441           t8('Print'),
1442           call => [ 'kivi.Order.show_print_options' ],
1443         ],
1444         action => [
1445           t8('E-mail'),
1446           call => [ 'kivi.Order.email' ],
1447         ],
1448         action => [
1449           t8('Download attachments of all parts'),
1450           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1451           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1452           only_if  => $::instance_conf->get_doc_storage,
1453         ],
1454       ], # end of combobox "Export"
1455
1456       action => [
1457         t8('Delete'),
1458         call     => [ 'kivi.Order.delete_order' ],
1459         confirm  => $::locale->text('Do you really want to delete this object?'),
1460         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1461         only_if  => $deletion_allowed,
1462       ],
1463     );
1464   }
1465 }
1466
1467 sub _create_pdf {
1468   my ($order, $pdf_ref, $params) = @_;
1469
1470   my @errors = ();
1471
1472   my $print_form = Form->new('');
1473   $print_form->{type}        = $order->type;
1474   $print_form->{formname}    = $params->{formname} || $order->type;
1475   $print_form->{format}      = $params->{format}   || 'pdf';
1476   $print_form->{media}       = $params->{media}    || 'file';
1477   $print_form->{groupitems}  = $params->{groupitems};
1478   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1479
1480   $order->language($params->{language});
1481   $order->flatten_to_form($print_form, format_amounts => 1);
1482
1483   # search for the template
1484   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1485     name        => $print_form->{formname},
1486     email       => $print_form->{media} eq 'email',
1487     language    => $params->{language},
1488     printer_id  => $print_form->{printer_id},  # todo
1489   );
1490
1491   if (!defined $template_file) {
1492     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);
1493   }
1494
1495   return @errors if scalar @errors;
1496
1497   $print_form->throw_on_error(sub {
1498     eval {
1499       $print_form->prepare_for_printing;
1500
1501       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1502         template  => $template_file,
1503         variables => $print_form,
1504         variable_content_types => {
1505           longdescription => 'html',
1506           partnotes       => 'html',
1507           notes           => 'html',
1508         },
1509       );
1510       1;
1511     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1512   });
1513
1514   return @errors;
1515 }
1516
1517 sub _get_files_for_email_dialog {
1518   my ($self) = @_;
1519
1520   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1521
1522   return %files if !$::instance_conf->get_doc_storage;
1523
1524   if ($self->order->id) {
1525     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
1526     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
1527     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
1528   }
1529
1530   my @parts =
1531     uniq_by { $_->{id} }
1532     map {
1533       +{ id         => $_->part->id,
1534          partnumber => $_->part->partnumber }
1535     } @{$self->order->items_sorted};
1536
1537   foreach my $part (@parts) {
1538     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1539     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1540   }
1541
1542   foreach my $key (keys %files) {
1543     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1544   }
1545
1546   return %files;
1547 }
1548
1549 sub _make_periodic_invoices_config_from_yaml {
1550   my ($yaml_config) = @_;
1551
1552   return if !$yaml_config;
1553   my $attr = YAML::Load($yaml_config);
1554   return if 'HASH' ne ref $attr;
1555   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1556 }
1557
1558
1559 sub _get_periodic_invoices_status {
1560   my ($self, $config) = @_;
1561
1562   return                      if $self->type ne _sales_order_type();
1563   return t8('not configured') if !$config;
1564
1565   my $active = ('HASH' eq ref $config)                           ? $config->{active}
1566              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1567              :                                                     die "Cannot get status of periodic invoices config";
1568
1569   return $active ? t8('active') : t8('inactive');
1570 }
1571
1572 sub _get_title_for {
1573   my ($self, $action) = @_;
1574
1575   return '' if none { lc($action)} qw(add edit);
1576
1577   # for locales:
1578   # $::locale->text("Add Sales Order");
1579   # $::locale->text("Add Purchase Order");
1580   # $::locale->text("Add Quotation");
1581   # $::locale->text("Add Request for Quotation");
1582   # $::locale->text("Edit Sales Order");
1583   # $::locale->text("Edit Purchase Order");
1584   # $::locale->text("Edit Quotation");
1585   # $::locale->text("Edit Request for Quotation");
1586
1587   $action = ucfirst(lc($action));
1588   return $self->type eq _sales_order_type()       ? $::locale->text("$action Sales Order")
1589        : $self->type eq _purchase_order_type()    ? $::locale->text("$action Purchase Order")
1590        : $self->type eq _sales_quotation_type()   ? $::locale->text("$action Quotation")
1591        : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
1592        : '';
1593 }
1594
1595 sub _sales_order_type {
1596   'sales_order';
1597 }
1598
1599 sub _purchase_order_type {
1600   'purchase_order';
1601 }
1602
1603 sub _sales_quotation_type {
1604   'sales_quotation';
1605 }
1606
1607 sub _request_quotation_type {
1608   'request_quotation';
1609 }
1610
1611 1;
1612
1613 __END__
1614
1615 =encoding utf-8
1616
1617 =head1 NAME
1618
1619 SL::Controller::Order - controller for orders
1620
1621 =head1 SYNOPSIS
1622
1623 This is a new form to enter orders, completely rewritten with the use
1624 of controller and java script techniques.
1625
1626 The aim is to provide the user a better expirience and a faster flow
1627 of work. Also the code should be more readable, more reliable and
1628 better to maintain.
1629
1630 =head2 Key Features
1631
1632 =over 4
1633
1634 =item *
1635
1636 One input row, so that input happens every time at the same place.
1637
1638 =item *
1639
1640 Use of pickers where possible.
1641
1642 =item *
1643
1644 Possibility to enter more than one item at once.
1645
1646 =item *
1647
1648 Save order only on "save" (and "save and delivery order"-workflow). No
1649 hidden save on "print" or "email".
1650
1651 =item *
1652
1653 Item list in a scrollable area, so that the workflow buttons stay at
1654 the bottom.
1655
1656 =item *
1657
1658 Reordering item rows with drag and drop is possible. Sorting item rows is
1659 possible (by partnumber, description, qty, sellprice and discount for now).
1660
1661 =item *
1662
1663 No C<update> is necessary. All entries and calculations are managed
1664 with ajax-calls and the page does only reload on C<save>.
1665
1666 =item *
1667
1668 User can see changes immediately, because of the use of java script
1669 and ajax.
1670
1671 =back
1672
1673 =head1 CODE
1674
1675 =head2 Layout
1676
1677 =over 4
1678
1679 =item * C<SL/Controller/Order.pm>
1680
1681 the controller
1682
1683 =item * C<template/webpages/order/form.html>
1684
1685 main form
1686
1687 =item * C<template/webpages/order/tabs/basic_data.html>
1688
1689 Main tab for basic_data.
1690
1691 This is the only tab here for now. "linked records" and "webdav" tabs are
1692 reused from generic code.
1693
1694 =over 4
1695
1696 =item * C<template/webpages/order/tabs/_item_input.html>
1697
1698 The input line for items
1699
1700 =item * C<template/webpages/order/tabs/_row.html>
1701
1702 One row for already entered items
1703
1704 =item * C<template/webpages/order/tabs/_tax_row.html>
1705
1706 Displaying tax information
1707
1708 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1709
1710 Dialog for entering more than one item at once
1711
1712 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1713
1714 Results for the filter in the multi items dialog
1715
1716 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1717
1718 Dialog for selecting price and discount sources
1719
1720 =back
1721
1722 =item * C<js/kivi.Order.js>
1723
1724 java script functions
1725
1726 =back
1727
1728 =head1 TODO
1729
1730 =over 4
1731
1732 =item * testing
1733
1734 =item * currency
1735
1736 =item * customer/vendor details ('D'-button)
1737
1738 =item * credit limit
1739
1740 =item * more workflows (save as new, quotation, purchase order)
1741
1742 =item * price sources: little symbols showing better price / better discount
1743
1744 =item * select units in input row?
1745
1746 =item * custom shipto address
1747
1748 =item * language / part translations
1749
1750 =item * access rights
1751
1752 =item * display weights
1753
1754 =item * history
1755
1756 =item * mtime check
1757
1758 =item * optional client/user behaviour
1759
1760 (transactions has to be set - department has to be set -
1761  force project if enabled in client config - transport cost reminder)
1762
1763 =back
1764
1765 =head1 KNOWN BUGS AND CAVEATS
1766
1767 =over 4
1768
1769 =item *
1770
1771 Customer discount is not displayed as a valid discount in price source popup
1772 (this might be a bug in price sources)
1773
1774 (I cannot reproduce this (Bernd))
1775
1776 =item *
1777
1778 No indication that <shift>-up/down expands/collapses second row.
1779
1780 =item *
1781
1782 Inline creation of parts is not currently supported
1783
1784 =item *
1785
1786 Table header is not sticky in the scrolling area.
1787
1788 =item *
1789
1790 Sorting does not include C<position>, neither does reordering.
1791
1792 This behavior was implemented intentionally. But we can discuss, which behavior
1793 should be implemented.
1794
1795 =item *
1796
1797 C<show_multi_items_dialog> does not use the currently inserted string for
1798 filtering.
1799
1800 =item *
1801
1802 The language selected in print or email dialog is not saved when the order is saved.
1803
1804 =back
1805
1806 =head1 To discuss / Nice to have
1807
1808 =over 4
1809
1810 =item *
1811
1812 How to expand/collapse second row. Now it can be done clicking the icon or
1813 <shift>-up/down.
1814
1815 =item *
1816
1817 Possibility to change longdescription in input row?
1818
1819 =item *
1820
1821 Possibility to select PriceSources in input row?
1822
1823 =item *
1824
1825 This controller uses a (changed) copy of the template for the PriceSource
1826 dialog. Maybe there could be used one code source.
1827
1828 =item *
1829
1830 Rounding-differences between this controller (PriceTaxCalculator) and the old
1831 form. This is not only a problem here, but also in all parts using the PTC.
1832 There exists a ticket and a patch. This patch should be testet.
1833
1834 =item *
1835
1836 An indicator, if the actual inputs are saved (like in an
1837 editor or on text processing application).
1838
1839 =item *
1840
1841 A warning when leaving the page without saveing unchanged inputs.
1842
1843 =item *
1844
1845 Workflows for delivery order and invoice are in the menu "Save", because the
1846 order is saved before opening the new document form. Nevertheless perhaps these
1847 workflow buttons should be put under "Workflows".
1848
1849
1850 =back
1851
1852 =head1 AUTHOR
1853
1854 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1855
1856 =cut