Auftrags-Controller: fake id für Items nach Workflow setzen.
[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   _setup_order_from_cv($self->order);
580   $self->_recalc();
581
582   my $cv_method = $self->cv;
583
584   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
585     $self->js->show('#cp_row');
586   } else {
587     $self->js->hide('#cp_row');
588   }
589
590   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
591     $self->js->show('#shipto_row');
592   } else {
593     $self->js->hide('#shipto_row');
594   }
595
596   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
597
598   $self->js
599     ->replaceWith('#order_cp_id',            $self->build_contact_select)
600     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
601     ->replaceWith('#business_info_row',      $self->build_business_info_row)
602     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
603     ->val(        '#order_taxincluded',      $self->order->taxincluded)
604     ->val(        '#order_payment_id',       $self->order->payment_id)
605     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
606     ->val(        '#order_intnotes',         $self->order->intnotes)
607     ->focus(      '#order_' . $self->cv . '_id');
608
609   $self->_js_redisplay_amounts_and_taxes;
610   $self->js->render();
611 }
612
613 # open the dialog for customer/vendor details
614 sub action_show_customer_vendor_details_dialog {
615   my ($self) = @_;
616
617   my $is_customer = 'customer' eq $::form->{vc};
618   my $cv;
619   if ($is_customer) {
620     $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
621   } else {
622     $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
623   }
624
625   my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
626   $details{discount_as_percent} = $cv->discount_as_percent;
627   $details{creditlimt}          = $cv->creditlimit_as_number;
628   $details{business}            = $cv->business->description      if $cv->business;
629   $details{language}            = $cv->language_obj->description  if $cv->language_obj;
630   $details{delivery_terms}      = $cv->delivery_term->description if $cv->delivery_term;
631   $details{payment_terms}       = $cv->payment->description       if $cv->payment;
632   $details{pricegroup}          = $cv->pricegroup->pricegroup     if $cv->pricegroup;
633
634   foreach my $entry (@{ $cv->shipto }) {
635     push @{ $details{SHIPTO} },   { map { $_ => $entry->$_ } @{$entry->meta->columns} };
636   }
637   foreach my $entry (@{ $cv->contacts }) {
638     push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
639   }
640
641   $_[0]->render('common/show_vc_details', { layout => 0 },
642                 is_customer => $is_customer,
643                 %details);
644
645 }
646
647 # called if a unit in an existing item row is changed
648 sub action_unit_changed {
649   my ($self) = @_;
650
651   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
652   my $item = $self->order->items_sorted->[$idx];
653
654   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
655   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
656
657   $self->_recalc();
658
659   $self->js
660     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
661   $self->_js_redisplay_line_values;
662   $self->_js_redisplay_amounts_and_taxes;
663   $self->js->render();
664 }
665
666 # add an item row for a new item entered in the input row
667 sub action_add_item {
668   my ($self) = @_;
669
670   my $form_attr = $::form->{add_item};
671
672   return unless $form_attr->{parts_id};
673
674   my $item = _new_item($self->order, $form_attr);
675
676   $self->order->add_items($item);
677
678   $self->_recalc();
679
680   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
681   my $row_as_html = $self->p->render('order/tabs/_row',
682                                      ITEM              => $item,
683                                      ID                => $item_id,
684                                      TYPE              => $self->type,
685                                      ALL_PRICE_FACTORS => $self->all_price_factors
686   );
687
688   $self->js
689     ->append('#row_table_id', $row_as_html);
690
691   if ( $item->part->is_assortment ) {
692     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
693     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
694       my $attr = { parts_id => $assortment_item->parts_id,
695                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
696                    unit     => $assortment_item->unit,
697                    description => $assortment_item->part->description,
698                  };
699       my $item = _new_item($self->order, $attr);
700
701       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
702       $item->discount(1) unless $assortment_item->charge;
703
704       $self->order->add_items( $item );
705       $self->_recalc();
706       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
707       my $row_as_html = $self->p->render('order/tabs/_row',
708                                          ITEM              => $item,
709                                          ID                => $item_id,
710                                          TYPE              => $self->type,
711                                          ALL_PRICE_FACTORS => $self->all_price_factors
712       );
713       $self->js
714         ->append('#row_table_id', $row_as_html);
715     };
716   };
717
718   $self->js
719     ->val('.add_item_input', '')
720     ->run('kivi.Order.init_row_handlers')
721     ->run('kivi.Order.row_table_scroll_down')
722     ->run('kivi.Order.renumber_positions')
723     ->focus('#add_item_parts_id_name');
724
725   $self->_js_redisplay_amounts_and_taxes;
726   $self->js->render();
727 }
728
729 # open the dialog for entering multiple items at once
730 sub action_show_multi_items_dialog {
731   require SL::DB::PartsGroup;
732   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
733                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
734 }
735
736 # update the filter results in the multi item dialog
737 sub action_multi_items_update_result {
738   my $max_count = 100;
739
740   $::form->{multi_items}->{filter}->{obsolete} = 0;
741
742   my $count = $_[0]->multi_items_models->count;
743
744   if ($count == 0) {
745     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
746     $_[0]->render($text, { layout => 0 });
747   } elsif ($count > $max_count) {
748     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
749     $_[0]->render($text, { layout => 0 });
750   } else {
751     my $multi_items = $_[0]->multi_items_models->get;
752     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
753                   multi_items => $multi_items);
754   }
755 }
756
757 # add item rows for multiple items at once
758 sub action_add_multi_items {
759   my ($self) = @_;
760
761   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
762   return $self->js->render() unless scalar @form_attr;
763
764   my @items;
765   foreach my $attr (@form_attr) {
766     my $item = _new_item($self->order, $attr);
767     push @items, $item;
768     if ( $item->part->is_assortment ) {
769       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
770         my $attr = { parts_id => $assortment_item->parts_id,
771                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
772                      unit     => $assortment_item->unit,
773                      description => $assortment_item->part->description,
774                    };
775         my $item = _new_item($self->order, $attr);
776
777         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
778         $item->discount(1) unless $assortment_item->charge;
779         push @items, $item;
780       }
781     }
782   }
783   $self->order->add_items(@items);
784
785   $self->_recalc();
786
787   foreach my $item (@items) {
788     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
789     my $row_as_html = $self->p->render('order/tabs/_row',
790                                        ITEM              => $item,
791                                        ID                => $item_id,
792                                        TYPE              => $self->type,
793                                        ALL_PRICE_FACTORS => $self->all_price_factors
794     );
795
796     $self->js->append('#row_table_id', $row_as_html);
797   }
798
799   $self->js
800     ->run('kivi.Order.close_multi_items_dialog')
801     ->run('kivi.Order.init_row_handlers')
802     ->run('kivi.Order.row_table_scroll_down')
803     ->run('kivi.Order.renumber_positions')
804     ->focus('#add_item_parts_id_name');
805
806   $self->_js_redisplay_amounts_and_taxes;
807   $self->js->render();
808 }
809
810 # recalculate all linetotals, amounts and taxes and redisplay them
811 sub action_recalc_amounts_and_taxes {
812   my ($self) = @_;
813
814   $self->_recalc();
815
816   $self->_js_redisplay_line_values;
817   $self->_js_redisplay_amounts_and_taxes;
818   $self->js->render();
819 }
820
821 # redisplay item rows if they are sorted by an attribute
822 sub action_reorder_items {
823   my ($self) = @_;
824
825   my %sort_keys = (
826     partnumber  => sub { $_[0]->part->partnumber },
827     description => sub { $_[0]->description },
828     qty         => sub { $_[0]->qty },
829     sellprice   => sub { $_[0]->sellprice },
830     discount    => sub { $_[0]->discount },
831   );
832
833   my $method = $sort_keys{$::form->{order_by}};
834   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
835   if ($::form->{sort_dir}) {
836     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
837   } else {
838     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
839   }
840   $self->js
841     ->run('kivi.Order.redisplay_items', \@to_sort)
842     ->render;
843 }
844
845 # show the popup to choose a price/discount source
846 sub action_price_popup {
847   my ($self) = @_;
848
849   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
850   my $item = $self->order->items_sorted->[$idx];
851
852   $self->render_price_dialog($item);
853 }
854
855 # get the longdescription for an item if the dialog to enter/change the
856 # longdescription was opened and the longdescription is empty
857 #
858 # If this item is new, get the longdescription from Part.
859 # Otherwise get it from OrderItem.
860 sub action_get_item_longdescription {
861   my $longdescription;
862
863   if ($::form->{item_id}) {
864     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
865   } elsif ($::form->{parts_id}) {
866     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
867   }
868   $_[0]->render(\ $longdescription, { type => 'text' });
869 }
870
871 # load the second row for one or more items
872 #
873 # This action gets the html code for all items second rows by rendering a template for
874 # the second row and sets the html code via client js.
875 sub action_load_second_rows {
876   my ($self) = @_;
877
878   $self->_recalc() if $self->order->is_sales; # for margin calculation
879
880   foreach my $item_id (@{ $::form->{item_ids} }) {
881     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
882     my $item = $self->order->items_sorted->[$idx];
883
884     $self->_js_load_second_row($item, $item_id, 0);
885   }
886
887   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
888
889   $self->js->render();
890 }
891
892 sub _js_load_second_row {
893   my ($self, $item, $item_id, $do_parse) = @_;
894
895   if ($do_parse) {
896     # Parse values from form (they are formated while rendering (template)).
897     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
898     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
899     foreach my $var (@{ $item->cvars_by_config }) {
900       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
901     }
902     $item->parse_custom_variable_values;
903   }
904
905   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
906
907   $self->js
908     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
909     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
910 }
911
912 sub _js_redisplay_line_values {
913   my ($self) = @_;
914
915   my $is_sales = $self->order->is_sales;
916
917   # sales orders with margins
918   my @data;
919   if ($is_sales) {
920     @data = map {
921       [
922        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
923        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
924        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
925       ]} @{ $self->order->items_sorted };
926   } else {
927     @data = map {
928       [
929        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
930       ]} @{ $self->order->items_sorted };
931   }
932
933   $self->js
934     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
935 }
936
937 sub _js_redisplay_amounts_and_taxes {
938   my ($self) = @_;
939
940   if (scalar @{ $self->{taxes} }) {
941     $self->js->show('#taxincluded_row_id');
942   } else {
943     $self->js->hide('#taxincluded_row_id');
944   }
945
946   if ($self->order->taxincluded) {
947     $self->js->hide('#subtotal_row_id');
948   } else {
949     $self->js->show('#subtotal_row_id');
950   }
951
952   $self->js
953     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
954     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
955     ->remove('.tax_row')
956     ->insertBefore($self->build_tax_rows, '#amount_row_id');
957 }
958
959 #
960 # helpers
961 #
962
963 sub init_valid_types {
964   [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
965 }
966
967 sub init_type {
968   my ($self) = @_;
969
970   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
971     die "Not a valid type for order";
972   }
973
974   $self->type($::form->{type});
975 }
976
977 sub init_cv {
978   my ($self) = @_;
979
980   my $cv = (any { $self->type eq $_ } (_sales_order_type(),    _sales_quotation_type()))   ? 'customer'
981          : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
982          : die "Not a valid type for order";
983
984   return $cv;
985 }
986
987 sub init_p {
988   SL::Presenter->get;
989 }
990
991 sub init_order {
992   $_[0]->_make_order;
993 }
994
995 # model used to filter/display the parts in the multi-items dialog
996 sub init_multi_items_models {
997   SL::Controller::Helper::GetModels->new(
998     controller     => $_[0],
999     model          => 'Part',
1000     with_objects   => [ qw(unit_obj) ],
1001     disable_plugin => 'paginated',
1002     source         => $::form->{multi_items},
1003     sorted         => {
1004       _default    => {
1005         by  => 'partnumber',
1006         dir => 1,
1007       },
1008       partnumber  => t8('Partnumber'),
1009       description => t8('Description')}
1010   );
1011 }
1012
1013 sub init_all_price_factors {
1014   SL::DB::Manager::PriceFactor->get_all;
1015 }
1016
1017 sub _check_auth {
1018   my ($self) = @_;
1019
1020   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1021
1022   my $right   = $right_for->{ $self->type };
1023   $right    ||= 'DOES_NOT_EXIST';
1024
1025   $::auth->assert($right);
1026 }
1027
1028 # build the selection box for contacts
1029 #
1030 # Needed, if customer/vendor changed.
1031 sub build_contact_select {
1032   my ($self) = @_;
1033
1034   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1035     value_key  => 'cp_id',
1036     title_key  => 'full_name_dep',
1037     default    => $self->order->cp_id,
1038     with_empty => 1,
1039     style      => 'width: 300px',
1040   );
1041 }
1042
1043 # build the selection box for shiptos
1044 #
1045 # Needed, if customer/vendor changed.
1046 sub build_shipto_select {
1047   my ($self) = @_;
1048
1049   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
1050     value_key  => 'shipto_id',
1051     title_key  => 'displayable_id',
1052     default    => $self->order->shipto_id,
1053     with_empty => 1,
1054     style      => 'width: 300px',
1055   );
1056 }
1057
1058 # render the info line for business
1059 #
1060 # Needed, if customer/vendor changed.
1061 sub build_business_info_row
1062 {
1063   $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1064 }
1065
1066 # build the rows for displaying taxes
1067 #
1068 # Called if amounts where recalculated and redisplayed.
1069 sub build_tax_rows {
1070   my ($self) = @_;
1071
1072   my $rows_as_html;
1073   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1074     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1075   }
1076   return $rows_as_html;
1077 }
1078
1079
1080 sub render_price_dialog {
1081   my ($self, $record_item) = @_;
1082
1083   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1084
1085   $self->js
1086     ->run(
1087       'kivi.io.price_chooser_dialog',
1088       t8('Available Prices'),
1089       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1090     )
1091     ->reinit_widgets;
1092
1093 #   if (@errors) {
1094 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1095 #     $self->js->show('#dialog_flash_error');
1096 #   }
1097
1098   $self->js->render;
1099 }
1100
1101 sub _load_order {
1102   my ($self) = @_;
1103
1104   return if !$::form->{id};
1105
1106   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
1107 }
1108
1109 # load or create a new order object
1110 #
1111 # And assign changes from the form to this object.
1112 # If the order is loaded from db, check if items are deleted in the form,
1113 # remove them form the object and collect them for removing from db on saving.
1114 # Then create/update items from form (via _make_item) and add them.
1115 sub _make_order {
1116   my ($self) = @_;
1117
1118   # add_items adds items to an order with no items for saving, but they cannot
1119   # be retrieved via items until the order is saved. Adding empty items to new
1120   # order here solves this problem.
1121   my $order;
1122   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1123   $order ||= SL::DB::Order->new(orderitems => [],
1124                                 quotation  => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
1125
1126   my $cv_id_method = $self->cv . '_id';
1127   if (!$::form->{id} && $::form->{$cv_id_method}) {
1128     $order->$cv_id_method($::form->{$cv_id_method});
1129     _setup_order_from_cv($order);
1130   }
1131
1132   my $form_orderitems               = delete $::form->{order}->{orderitems};
1133   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1134
1135   $order->assign_attributes(%{$::form->{order}});
1136
1137   my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1138   $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1139
1140   # remove deleted items
1141   $self->item_ids_to_delete([]);
1142   foreach my $idx (reverse 0..$#{$order->orderitems}) {
1143     my $item = $order->orderitems->[$idx];
1144     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1145       splice @{$order->orderitems}, $idx, 1;
1146       push @{$self->item_ids_to_delete}, $item->id;
1147     }
1148   }
1149
1150   my @items;
1151   my $pos = 1;
1152   foreach my $form_attr (@{$form_orderitems}) {
1153     my $item = _make_item($order, $form_attr);
1154     $item->position($pos);
1155     push @items, $item;
1156     $pos++;
1157   }
1158   $order->add_items(grep {!$_->id} @items);
1159
1160   return $order;
1161 }
1162
1163 # create or update items from form
1164 #
1165 # Make item objects from form values. For items already existing read from db.
1166 # Create a new item else. And assign attributes.
1167 sub _make_item {
1168   my ($record, $attr) = @_;
1169
1170   my $item;
1171   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1172
1173   my $is_new = !$item;
1174
1175   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1176   # they cannot be retrieved via custom_variables until the order/orderitem is
1177   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1178   $item ||= SL::DB::OrderItem->new(custom_variables => []);
1179
1180   $item->assign_attributes(%$attr);
1181   $item->longdescription($item->part->notes)                     if $is_new && !defined $attr->{longdescription};
1182   $item->project_id($record->globalproject_id)                   if $is_new && !defined $attr->{project_id};
1183   $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1184
1185   return $item;
1186 }
1187
1188 # create a new item
1189 #
1190 # This is used to add one item
1191 sub _new_item {
1192   my ($record, $attr) = @_;
1193
1194   my $item = SL::DB::OrderItem->new;
1195   $item->assign_attributes(%$attr);
1196
1197   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
1198   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1199
1200   $item->unit($part->unit) if !$item->unit;
1201
1202   my $price_src;
1203   if ( $part->is_assortment ) {
1204     # add assortment items with price 0, as the components carry the price
1205     $price_src = $price_source->price_from_source("");
1206     $price_src->price(0);
1207   } elsif ($item->sellprice) {
1208     $price_src = $price_source->price_from_source("");
1209     $price_src->price($item->sellprice);
1210   } else {
1211     $price_src = $price_source->best_price
1212            ? $price_source->best_price
1213            : $price_source->price_from_source("");
1214     $price_src->price(0) if !$price_source->best_price;
1215   }
1216
1217   my $discount_src;
1218   if ($item->discount) {
1219     $discount_src = $price_source->discount_from_source("");
1220     $discount_src->discount($item->discount);
1221   } else {
1222     $discount_src = $price_source->best_discount
1223                   ? $price_source->best_discount
1224                   : $price_source->discount_from_source("");
1225     $discount_src->discount(0) if !$price_source->best_discount;
1226   }
1227
1228   my %new_attr;
1229   $new_attr{part}                   = $part;
1230   $new_attr{description}            = $part->description     if ! $item->description;
1231   $new_attr{qty}                    = 1.0                    if ! $item->qty;
1232   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
1233   $new_attr{sellprice}              = $price_src->price;
1234   $new_attr{discount}               = $discount_src->discount;
1235   $new_attr{active_price_source}    = $price_src;
1236   $new_attr{active_discount_source} = $discount_src;
1237   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
1238   $new_attr{project_id}             = $record->globalproject_id;
1239   $new_attr{lastcost}               = $record->is_sales ? $part->lastcost : 0;
1240
1241   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1242   # they cannot be retrieved via custom_variables until the order/orderitem is
1243   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1244   $new_attr{custom_variables} = [];
1245
1246   $item->assign_attributes(%new_attr);
1247
1248   return $item;
1249 }
1250
1251 sub _setup_order_from_cv {
1252   my ($order) = @_;
1253
1254   $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
1255
1256   $order->intnotes($order->customervendor->notes);
1257
1258   if ($order->is_sales) {
1259     $order->salesman_id($order->customer->salesman_id);
1260     $order->taxincluded(defined($order->customer->taxincluded_checked)
1261                         ? $order->customer->taxincluded_checked
1262                         : $::myconfig{taxincluded_checked});
1263   }
1264
1265 }
1266
1267 # recalculate prices and taxes
1268 #
1269 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1270 sub _recalc {
1271   my ($self) = @_;
1272
1273   # bb: todo: currency later
1274   $self->order->currency_id($::instance_conf->get_currency_id());
1275
1276   my %pat = $self->order->calculate_prices_and_taxes();
1277   $self->{taxes} = [];
1278   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1279     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1280
1281     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1282     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
1283                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1284                                 tax       => $tax });
1285   }
1286
1287   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1288 }
1289
1290 # get data for saving, printing, ..., that is not changed in the form
1291 #
1292 # Only cvars for now.
1293 sub _get_unalterable_data {
1294   my ($self) = @_;
1295
1296   foreach my $item (@{ $self->order->items }) {
1297     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1298     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1299     foreach my $var (@{ $item->cvars_by_config }) {
1300       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1301     }
1302     $item->parse_custom_variable_values;
1303   }
1304 }
1305
1306 # delete the order
1307 #
1308 # And remove related files in the spool directory
1309 sub _delete {
1310   my ($self) = @_;
1311
1312   my $errors = [];
1313   my $db     = $self->order->db;
1314
1315   $db->with_transaction(
1316     sub {
1317       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1318       $self->order->delete;
1319       my $spool = $::lx_office_conf{paths}->{spool};
1320       unlink map { "$spool/$_" } @spoolfiles if $spool;
1321
1322       1;
1323   }) || push(@{$errors}, $db->error);
1324
1325   return $errors;
1326 }
1327
1328 # save the order
1329 #
1330 # And delete items that are deleted in the form.
1331 sub _save {
1332   my ($self) = @_;
1333
1334   my $errors = [];
1335   my $db     = $self->order->db;
1336
1337   $db->with_transaction(sub {
1338     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1339     $self->order->save(cascade => 1);
1340
1341     # link records
1342     if ($::form->{converted_from_oe_id}) {
1343       SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
1344
1345       if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1346         my $idx = 0;
1347         foreach (@{ $self->order->items_sorted }) {
1348           my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1349           next if !$from_id;
1350           SL::DB::RecordLink->new(from_table => 'orderitems',
1351                                   from_id    => $from_id,
1352                                   to_table   => 'orderitems',
1353                                   to_id      => $_->id
1354           )->save;
1355           $idx++;
1356         }
1357       }
1358     }
1359     1;
1360   }) || push(@{$errors}, $db->error);
1361
1362   return $errors;
1363 }
1364
1365 sub _workflow_sales_or_purchase_order {
1366   my ($self) = @_;
1367
1368   my $destination_type = $::form->{type} eq _sales_quotation_type()   ? _sales_order_type()
1369                        : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
1370                        : '';
1371
1372   $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1373   $self->{converted_from_oe_id} = delete $::form->{id};
1374
1375   # set item ids to new fake id, to identify them as new items
1376   foreach my $item (@{$self->order->items_sorted}) {
1377     $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1378   }
1379
1380   # change form type
1381   $::form->{type} = $destination_type;
1382   $self->init_type;
1383   $self->_check_auth;
1384
1385   $self->_recalc();
1386   $self->_get_unalterable_data();
1387   $self->_pre_render();
1388
1389   # trigger rendering values for second row/longdescription as hidden,
1390   # because they are loaded only on demand. So we need to keep the values
1391   # from the source.
1392   $_->{render_second_row}      = 1 for @{ $self->order->items_sorted };
1393   $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1394
1395   $self->render(
1396     'order/form',
1397     title => $self->_get_title_for('edit'),
1398     %{$self->{template_args}}
1399   );
1400 }
1401
1402
1403 sub _pre_render {
1404   my ($self) = @_;
1405
1406   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
1407   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
1408   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1409                                                                                             deleted => 0 ] ],
1410                                                                          sort_by => 'name');
1411   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1412                                                                                             deleted => 0 ] ],
1413                                                                          sort_by => 'name');
1414   $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1415                                                                                            active => 1 ] ],
1416                                                                         sort_by => 'projectnumber');
1417   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1418                                                                                                       obsolete => 0 ] ]);
1419   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1420   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
1421   $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
1422   $self->{order_probabilities}      = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1423
1424   my $print_form = Form->new('');
1425   $print_form->{type}      = $self->type;
1426   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
1427   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1428   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
1429     form => $print_form,
1430     options => {dialog_name_prefix => 'print_options.',
1431                 show_headers       => 1,
1432                 no_queue           => 1,
1433                 no_postscript      => 1,
1434                 no_opendocument    => 1,
1435                 no_html            => 1},
1436   );
1437
1438   foreach my $item (@{$self->order->orderitems}) {
1439     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1440     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1441     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1442   }
1443
1444   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1445     my $webdav = SL::Webdav->new(
1446       type     => $self->type,
1447       number   => $self->order->ordnumber,
1448     );
1449     my @all_objects = $webdav->get_all_objects;
1450     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1451                                                     type => t8('File'),
1452                                                     link => File::Spec->catfile($_->full_filedescriptor),
1453                                                 } } @all_objects;
1454   }
1455
1456   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
1457   $self->_setup_edit_action_bar;
1458 }
1459
1460 sub _setup_edit_action_bar {
1461   my ($self, %params) = @_;
1462
1463   my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
1464                       || (($self->type eq _sales_order_type())    && $::instance_conf->get_sales_order_show_delete)
1465                       || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1466
1467   for my $bar ($::request->layout->get('actionbar')) {
1468     $bar->add(
1469       combobox => [
1470         action => [
1471           t8('Save'),
1472           call      => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts ],
1473           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1474         ],
1475         action => [
1476           t8('Save as new'),
1477           call      => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1478           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1479           disabled  => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1480         ],
1481         action => [
1482           t8('Save and Delivery Order'),
1483           call      => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1484           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1485           only_if   => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
1486         ],
1487         action => [
1488           t8('Save and Invoice'),
1489           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1490           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1491         ],
1492       ], # end of combobox "Save"
1493
1494       combobox => [
1495         action => [
1496           t8('Workflow'),
1497         ],
1498         action => [
1499           t8('Sales Order'),
1500           submit   => [ '#order_form', { action => "Order/sales_order" } ],
1501           only_if  => (any { $self->type eq $_ } (_sales_quotation_type())),
1502           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1503         ],
1504         action => [
1505           t8('Purchase Order'),
1506           submit   => [ '#order_form', { action => "Order/purchase_order" } ],
1507           only_if  => (any { $self->type eq $_ } (_request_quotation_type())),
1508           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1509         ],
1510       ], # end of combobox "Workflow"
1511
1512       combobox => [
1513         action => [
1514           t8('Export'),
1515         ],
1516         action => [
1517           t8('Print'),
1518           call => [ 'kivi.Order.show_print_options' ],
1519         ],
1520         action => [
1521           t8('E-mail'),
1522           call => [ 'kivi.Order.email' ],
1523         ],
1524         action => [
1525           t8('Download attachments of all parts'),
1526           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1527           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1528           only_if  => $::instance_conf->get_doc_storage,
1529         ],
1530       ], # end of combobox "Export"
1531
1532       action => [
1533         t8('Delete'),
1534         call     => [ 'kivi.Order.delete_order' ],
1535         confirm  => $::locale->text('Do you really want to delete this object?'),
1536         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1537         only_if  => $deletion_allowed,
1538       ],
1539     );
1540   }
1541 }
1542
1543 sub _create_pdf {
1544   my ($order, $pdf_ref, $params) = @_;
1545
1546   my @errors = ();
1547
1548   my $print_form = Form->new('');
1549   $print_form->{type}        = $order->type;
1550   $print_form->{formname}    = $params->{formname} || $order->type;
1551   $print_form->{format}      = $params->{format}   || 'pdf';
1552   $print_form->{media}       = $params->{media}    || 'file';
1553   $print_form->{groupitems}  = $params->{groupitems};
1554   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1555
1556   $order->language($params->{language});
1557   $order->flatten_to_form($print_form, format_amounts => 1);
1558
1559   # search for the template
1560   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1561     name        => $print_form->{formname},
1562     email       => $print_form->{media} eq 'email',
1563     language    => $params->{language},
1564     printer_id  => $print_form->{printer_id},  # todo
1565   );
1566
1567   if (!defined $template_file) {
1568     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);
1569   }
1570
1571   return @errors if scalar @errors;
1572
1573   $print_form->throw_on_error(sub {
1574     eval {
1575       $print_form->prepare_for_printing;
1576
1577       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1578         template  => $template_file,
1579         variables => $print_form,
1580         variable_content_types => {
1581           longdescription => 'html',
1582           partnotes       => 'html',
1583           notes           => 'html',
1584         },
1585       );
1586       1;
1587     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1588   });
1589
1590   return @errors;
1591 }
1592
1593 sub _get_files_for_email_dialog {
1594   my ($self) = @_;
1595
1596   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1597
1598   return %files if !$::instance_conf->get_doc_storage;
1599
1600   if ($self->order->id) {
1601     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
1602     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
1603     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
1604   }
1605
1606   my @parts =
1607     uniq_by { $_->{id} }
1608     map {
1609       +{ id         => $_->part->id,
1610          partnumber => $_->part->partnumber }
1611     } @{$self->order->items_sorted};
1612
1613   foreach my $part (@parts) {
1614     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1615     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1616   }
1617
1618   foreach my $key (keys %files) {
1619     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1620   }
1621
1622   return %files;
1623 }
1624
1625 sub _make_periodic_invoices_config_from_yaml {
1626   my ($yaml_config) = @_;
1627
1628   return if !$yaml_config;
1629   my $attr = YAML::Load($yaml_config);
1630   return if 'HASH' ne ref $attr;
1631   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1632 }
1633
1634
1635 sub _get_periodic_invoices_status {
1636   my ($self, $config) = @_;
1637
1638   return                      if $self->type ne _sales_order_type();
1639   return t8('not configured') if !$config;
1640
1641   my $active = ('HASH' eq ref $config)                           ? $config->{active}
1642              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1643              :                                                     die "Cannot get status of periodic invoices config";
1644
1645   return $active ? t8('active') : t8('inactive');
1646 }
1647
1648 sub _get_title_for {
1649   my ($self, $action) = @_;
1650
1651   return '' if none { lc($action)} qw(add edit);
1652
1653   # for locales:
1654   # $::locale->text("Add Sales Order");
1655   # $::locale->text("Add Purchase Order");
1656   # $::locale->text("Add Quotation");
1657   # $::locale->text("Add Request for Quotation");
1658   # $::locale->text("Edit Sales Order");
1659   # $::locale->text("Edit Purchase Order");
1660   # $::locale->text("Edit Quotation");
1661   # $::locale->text("Edit Request for Quotation");
1662
1663   $action = ucfirst(lc($action));
1664   return $self->type eq _sales_order_type()       ? $::locale->text("$action Sales Order")
1665        : $self->type eq _purchase_order_type()    ? $::locale->text("$action Purchase Order")
1666        : $self->type eq _sales_quotation_type()   ? $::locale->text("$action Quotation")
1667        : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
1668        : '';
1669 }
1670
1671 sub _sales_order_type {
1672   'sales_order';
1673 }
1674
1675 sub _purchase_order_type {
1676   'purchase_order';
1677 }
1678
1679 sub _sales_quotation_type {
1680   'sales_quotation';
1681 }
1682
1683 sub _request_quotation_type {
1684   'request_quotation';
1685 }
1686
1687 1;
1688
1689 __END__
1690
1691 =encoding utf-8
1692
1693 =head1 NAME
1694
1695 SL::Controller::Order - controller for orders
1696
1697 =head1 SYNOPSIS
1698
1699 This is a new form to enter orders, completely rewritten with the use
1700 of controller and java script techniques.
1701
1702 The aim is to provide the user a better expirience and a faster flow
1703 of work. Also the code should be more readable, more reliable and
1704 better to maintain.
1705
1706 =head2 Key Features
1707
1708 =over 4
1709
1710 =item *
1711
1712 One input row, so that input happens every time at the same place.
1713
1714 =item *
1715
1716 Use of pickers where possible.
1717
1718 =item *
1719
1720 Possibility to enter more than one item at once.
1721
1722 =item *
1723
1724 Save order only on "save" (and "save and delivery order"-workflow). No
1725 hidden save on "print" or "email".
1726
1727 =item *
1728
1729 Item list in a scrollable area, so that the workflow buttons stay at
1730 the bottom.
1731
1732 =item *
1733
1734 Reordering item rows with drag and drop is possible. Sorting item rows is
1735 possible (by partnumber, description, qty, sellprice and discount for now).
1736
1737 =item *
1738
1739 No C<update> is necessary. All entries and calculations are managed
1740 with ajax-calls and the page does only reload on C<save>.
1741
1742 =item *
1743
1744 User can see changes immediately, because of the use of java script
1745 and ajax.
1746
1747 =back
1748
1749 =head1 CODE
1750
1751 =head2 Layout
1752
1753 =over 4
1754
1755 =item * C<SL/Controller/Order.pm>
1756
1757 the controller
1758
1759 =item * C<template/webpages/order/form.html>
1760
1761 main form
1762
1763 =item * C<template/webpages/order/tabs/basic_data.html>
1764
1765 Main tab for basic_data.
1766
1767 This is the only tab here for now. "linked records" and "webdav" tabs are
1768 reused from generic code.
1769
1770 =over 4
1771
1772 =item * C<template/webpages/order/tabs/_business_info_row.html>
1773
1774 For displaying information on business type
1775
1776 =item * C<template/webpages/order/tabs/_item_input.html>
1777
1778 The input line for items
1779
1780 =item * C<template/webpages/order/tabs/_row.html>
1781
1782 One row for already entered items
1783
1784 =item * C<template/webpages/order/tabs/_tax_row.html>
1785
1786 Displaying tax information
1787
1788 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1789
1790 Dialog for entering more than one item at once
1791
1792 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1793
1794 Results for the filter in the multi items dialog
1795
1796 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1797
1798 Dialog for selecting price and discount sources
1799
1800 =back
1801
1802 =item * C<js/kivi.Order.js>
1803
1804 java script functions
1805
1806 =back
1807
1808 =head1 TODO
1809
1810 =over 4
1811
1812 =item * testing
1813
1814 =item * currency
1815
1816 =item * credit limit
1817
1818 =item * more workflows (save as new, quotation, purchase order)
1819
1820 =item * price sources: little symbols showing better price / better discount
1821
1822 =item * select units in input row?
1823
1824 =item * custom shipto address
1825
1826 =item * language / part translations
1827
1828 =item * access rights
1829
1830 =item * display weights
1831
1832 =item * history
1833
1834 =item * mtime check
1835
1836 =item * optional client/user behaviour
1837
1838 (transactions has to be set - department has to be set -
1839  force project if enabled in client config - transport cost reminder)
1840
1841 =back
1842
1843 =head1 KNOWN BUGS AND CAVEATS
1844
1845 =over 4
1846
1847 =item *
1848
1849 Customer discount is not displayed as a valid discount in price source popup
1850 (this might be a bug in price sources)
1851
1852 (I cannot reproduce this (Bernd))
1853
1854 =item *
1855
1856 No indication that <shift>-up/down expands/collapses second row.
1857
1858 =item *
1859
1860 Inline creation of parts is not currently supported
1861
1862 =item *
1863
1864 Table header is not sticky in the scrolling area.
1865
1866 =item *
1867
1868 Sorting does not include C<position>, neither does reordering.
1869
1870 This behavior was implemented intentionally. But we can discuss, which behavior
1871 should be implemented.
1872
1873 =item *
1874
1875 C<show_multi_items_dialog> does not use the currently inserted string for
1876 filtering.
1877
1878 =item *
1879
1880 The language selected in print or email dialog is not saved when the order is saved.
1881
1882 =back
1883
1884 =head1 To discuss / Nice to have
1885
1886 =over 4
1887
1888 =item *
1889
1890 How to expand/collapse second row. Now it can be done clicking the icon or
1891 <shift>-up/down.
1892
1893 =item *
1894
1895 Possibility to change longdescription in input row?
1896
1897 =item *
1898
1899 Possibility to select PriceSources in input row?
1900
1901 =item *
1902
1903 This controller uses a (changed) copy of the template for the PriceSource
1904 dialog. Maybe there could be used one code source.
1905
1906 =item *
1907
1908 Rounding-differences between this controller (PriceTaxCalculator) and the old
1909 form. This is not only a problem here, but also in all parts using the PTC.
1910 There exists a ticket and a patch. This patch should be testet.
1911
1912 =item *
1913
1914 An indicator, if the actual inputs are saved (like in an
1915 editor or on text processing application).
1916
1917 =item *
1918
1919 A warning when leaving the page without saveing unchanged inputs.
1920
1921 =item *
1922
1923 Workflows for delivery order and invoice are in the menu "Save", because the
1924 order is saved before opening the new document form. Nevertheless perhaps these
1925 workflow buttons should be put under "Workflows".
1926
1927
1928 =back
1929
1930 =head1 AUTHOR
1931
1932 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1933
1934 =cut