c129999b088ebcda4e12f5eb6409d096eb971939
[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;
7 use SL::Presenter;
8 use SL::Locale::String;
9 use SL::SessionFile::Random;
10 use SL::PriceSource;
11 use SL::Form;
12 use SL::Webdav;
13 use SL::Template;
14
15 use SL::DB::Order;
16 use SL::DB::Customer;
17 use SL::DB::Vendor;
18 use SL::DB::TaxZone;
19 use SL::DB::Employee;
20 use SL::DB::Project;
21 use SL::DB::Default;
22 use SL::DB::Unit;
23 use SL::DB::Price;
24 use SL::DB::PriceFactor;
25 use SL::DB::Part;
26 use SL::DB::Printer;
27 use SL::DB::Language;
28
29 use SL::Helper::DateTime;
30 use SL::Helper::CreatePDF qw(:all);
31 use SL::Helper::PrintOptions;
32
33 use SL::Controller::Helper::GetModels;
34
35 use List::Util qw(max first);
36 use List::MoreUtils qw(none pairwise first_index);
37 use English qw(-no_match_vars);
38 use File::Spec;
39
40 use Rose::Object::MakeMethods::Generic
41 (
42  scalar => [ qw(item_ids_to_delete) ],
43  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
44 );
45
46
47 # safety
48 __PACKAGE__->run_before('_check_auth');
49
50 __PACKAGE__->run_before('_recalc',
51                         only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
52
53 __PACKAGE__->run_before('_get_unalterable_data',
54                         only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
55
56 #
57 # actions
58 #
59
60 # add a new order
61 sub action_add {
62   my ($self) = @_;
63
64   $self->order->transdate(DateTime->now_local());
65   $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
66
67   $self->_pre_render();
68   $self->render(
69     'order/form',
70     title => $self->type eq _sales_order_type()    ? $::locale->text('Add Sales Order')
71            : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
72            : '',
73     %{$self->{template_args}}
74   );
75 }
76
77 # edit an existing order
78 sub action_edit {
79   my ($self) = @_;
80
81   $self->_load_order;
82   $self->_recalc();
83   $self->_pre_render();
84   $self->render(
85     'order/form',
86     title => $self->type eq _sales_order_type()    ? $::locale->text('Edit Sales Order')
87            : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
88            : '',
89     %{$self->{template_args}}
90   );
91 }
92
93 # delete the order
94 sub action_delete {
95   my ($self) = @_;
96
97   my $errors = $self->_delete();
98
99   if (scalar @{ $errors }) {
100     $self->js->flash('error', $_) foreach @{ $errors };
101     return $self->js->render();
102   }
103
104   flash_later('info', $::locale->text('The order has been deleted'));
105   my @redirect_params = (
106     action => 'edit',
107     type   => $self->type,
108   );
109
110   $self->redirect_to(@redirect_params);
111 }
112
113 # save the order
114 sub action_save {
115   my ($self) = @_;
116
117   my $errors = $self->_save();
118
119   if (scalar @{ $errors }) {
120     $self->js->flash('error', $_) foreach @{ $errors };
121     return $self->js->render();
122   }
123
124   flash_later('info', $::locale->text('The order has been saved'));
125   my @redirect_params = (
126     action => 'edit',
127     type   => $self->type,
128     id     => $self->order->id,
129   );
130
131   $self->redirect_to(@redirect_params);
132 }
133
134 # print the order
135 #
136 # This is called if "print" is pressed in the print dialog.
137 # If PDF creation was requested and succeeded, the pdf is stored in a session
138 # file and the filename is stored as session value with an unique key. A
139 # javascript function with this key is then called. This function calls the
140 # download action below (action_download_pdf), which offers the file for
141 # download.
142 sub action_print {
143   my ($self) = @_;
144
145   my $format      = $::form->{print_options}->{format};
146   my $media       = $::form->{print_options}->{media};
147   my $formname    = $::form->{print_options}->{formname};
148   my $copies      = $::form->{print_options}->{copies};
149   my $groupitems  = $::form->{print_options}->{groupitems};
150
151   # only pdf by now
152   if (none { $format eq $_ } qw(pdf)) {
153     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
154   }
155
156   # only screen or printer by now
157   if (none { $media eq $_ } qw(screen printer)) {
158     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
159   }
160
161   my $language;
162   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
163
164   my $form = Form->new;
165   $form->{ordnumber} = $self->order->ordnumber;
166   $form->{type}      = $self->type;
167   $form->{format}    = $format;
168   $form->{formname}  = $formname;
169   $form->{language}  = '_' . $language->template_code if $language;
170   my $pdf_filename   = $form->generate_attachment_filename();
171
172   my $pdf;
173   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
174                                                   formname   => $formname,
175                                                   language   => $language,
176                                                   groupitems => $groupitems });
177   if (scalar @errors) {
178     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
179   }
180
181   if ($media eq 'screen') {
182     # screen/download
183     my $sfile = SL::SessionFile::Random->new(mode => "w");
184     $sfile->fh->print($pdf);
185     $sfile->fh->close;
186
187     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
188     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
189
190     $self->js
191     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
192     ->flash('info', t8('The PDF has been created'));
193
194   } elsif ($media eq 'printer') {
195     # printer
196     my $printer_id = $::form->{print_options}->{printer_id};
197     SL::DB::Printer->new(id => $printer_id)->load->print_document(
198       copies  => $copies,
199       content => $pdf,
200     );
201
202     $self->js->flash('info', t8('The PDF has been printed'));
203   }
204
205   # copy file to webdav folder
206   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
207     my $webdav = SL::Webdav->new(
208       type     => $self->type,
209       number   => $self->order->ordnumber,
210     );
211     my $webdav_file = SL::Webdav::File->new(
212       webdav   => $webdav,
213       filename => $pdf_filename,
214     );
215     eval {
216       $webdav_file->store(data => \$pdf);
217       1;
218     } or do {
219       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
220     }
221   }
222
223   $self->js->render;
224 }
225
226 # offer pdf for download
227 #
228 # It needs to get the key for the session value to get the pdf file.
229 sub action_download_pdf {
230   my ($self) = @_;
231
232   my $key = $::form->{key};
233   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
234   return $self->send_file(
235     $tmp_filename,
236     type => 'application/pdf',
237     name => $::form->{pdf_filename},
238   );
239 }
240
241 # open the email dialog
242 sub action_show_email_dialog {
243   my ($self) = @_;
244
245   my $cv_method = $self->cv;
246
247   if (!$self->order->$cv_method) {
248     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'))
249                     ->render($self);
250   }
251
252   $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
253   $self->{email}->{to} ||= $self->order->$cv_method->email;
254   $self->{email}->{cc}   = $self->order->$cv_method->cc;
255   $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
256   # Todo: get addresses from shipto, if any
257
258   my $form = Form->new;
259   $form->{ordnumber} = $self->order->ordnumber;
260   $form->{formname}  = $self->type;
261   $form->{type}      = $self->type;
262   $form->{language} = 'de';
263   $form->{format}   = 'pdf';
264
265   $self->{email}->{subject}             = $form->generate_email_subject();
266   $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
267   $self->{email}->{message}             = $form->create_email_signature();
268
269   my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
270   $self->js
271       ->run('kivi.Order.show_email_dialog', $dialog_html)
272       ->reinit_widgets
273       ->render($self);
274 }
275
276 # send email
277 #
278 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
279 sub action_send_email {
280   my ($self) = @_;
281
282   my $mail      = Mailer->new;
283   $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
284   $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
285
286   my $pdf;
287   my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
288   if (scalar @errors) {
289     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
290   }
291
292   $mail->{attachments} = [{ "content" => $pdf,
293                             "name"    => $::form->{email}->{attachment_filename} }];
294
295   if (my $err = $mail->send) {
296     return $self->js->flash('error', t8('Sending E-mail: ') . $err)
297                     ->render($self);
298   }
299
300   # internal notes
301   my $intnotes = $self->order->intnotes;
302   $intnotes   .= "\n\n" if $self->order->intnotes;
303   $intnotes   .= t8('[email]')                                                                                        . "\n";
304   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
305   $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
306   $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
307   $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
308   $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
309   $intnotes   .= t8('Message')    . ": " . $mail->{message};
310
311   $self->js
312       ->val('#order_intnotes', $intnotes)
313       ->run('kivi.Order.close_email_dialog')
314       ->render($self);
315 }
316
317 # save the order and redirect to the frontend subroutine for a new
318 # delivery order
319 sub action_save_and_delivery_order {
320   my ($self) = @_;
321
322   my $errors = $self->_save();
323
324   if (scalar @{ $errors }) {
325     $self->js->flash('error', $_) foreach @{ $errors };
326     return $self->js->render();
327   }
328   flash_later('info', $::locale->text('The order has been saved'));
329
330   my @redirect_params = (
331     controller => 'oe.pl',
332     action     => 'oe_delivery_order_from_order',
333     id         => $self->order->id,
334   );
335
336   $self->redirect_to(@redirect_params);
337 }
338
339 # set form elements in respect of a changed customer or vendor
340 #
341 # This action is called on an change of the customer/vendor picker.
342 sub action_customer_vendor_changed {
343   my ($self) = @_;
344
345   my $cv_method = $self->cv;
346
347   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
348     $self->js->show('#cp_row');
349   } else {
350     $self->js->hide('#cp_row');
351   }
352
353   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
354     $self->js->show('#shipto_row');
355   } else {
356     $self->js->hide('#shipto_row');
357   }
358
359   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
360
361   if ($self->order->is_sales) {
362     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
363                               ? $self->order->$cv_method->taxincluded_checked
364                               : $::myconfig{taxincluded_checked});
365   }
366
367   $self->order->payment_id($self->order->$cv_method->payment_id);
368   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
369
370   $self->_recalc();
371
372   $self->js
373     ->replaceWith('#order_cp_id',            $self->build_contact_select)
374     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
375     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
376     ->val(        '#order_taxincluded',      $self->order->taxincluded)
377     ->val(        '#order_payment_id',       $self->order->payment_id)
378     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
379     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
380     ->focus(      '#order_' . $self->cv . '_id');
381
382   $self->_js_redisplay_amounts_and_taxes;
383   $self->js->render();
384 }
385
386 # called if a unit in an existing item row is changed
387 sub action_unit_changed {
388   my ($self) = @_;
389
390   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
391   my $item = $self->order->items_sorted->[$idx];
392
393   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
394   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
395
396   $self->_recalc();
397
398   $self->js
399     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
400   $self->_js_redisplay_linetotals;
401   $self->_js_redisplay_amounts_and_taxes;
402   $self->js->render();
403 }
404
405 # add an item row for a new item entered in the input row
406 sub action_add_item {
407   my ($self) = @_;
408
409   my $form_attr = $::form->{add_item};
410
411   return unless $form_attr->{parts_id};
412
413   my $item = _new_item($self->order, $form_attr);
414   $self->order->add_items($item);
415
416   $self->_recalc();
417
418   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
419   my $row_as_html = $self->p->render('order/tabs/_row',
420                                      ITEM              => $item,
421                                      ID                => $item_id,
422                                      ALL_PRICE_FACTORS => $self->all_price_factors
423   );
424
425   $self->js
426     ->append('#row_table_id', $row_as_html)
427     ->val('.add_item_input', '')
428     ->run('kivi.Order.init_row_handlers')
429     ->run('kivi.Order.row_table_scroll_down')
430     ->run('kivi.Order.renumber_positions')
431     ->focus('#add_item_parts_id_name');
432
433   $self->_js_redisplay_amounts_and_taxes;
434   $self->js->render();
435 }
436
437 # open the dialog for entering multiple items at once
438 sub action_show_multi_items_dialog {
439   require SL::DB::PartsGroup;
440   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
441                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
442 }
443
444 # update the filter results in the multi item dialog
445 sub action_multi_items_update_result {
446   my $max_count = 100;
447
448   $::form->{multi_items}->{filter}->{obsolete} = 0;
449
450   my $count = $_[0]->multi_items_models->count;
451
452   if ($count == 0) {
453     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
454     $_[0]->render($text, { layout => 0 });
455   } elsif ($count > $max_count) {
456     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
457     $_[0]->render($text, { layout => 0 });
458   } else {
459     my $multi_items = $_[0]->multi_items_models->get;
460     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
461                   multi_items => $multi_items);
462   }
463 }
464
465 # add item rows for multiple items add once
466 sub action_add_multi_items {
467   my ($self) = @_;
468
469   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
470   return $self->js->render() unless scalar @form_attr;
471
472   my @items;
473   foreach my $attr (@form_attr) {
474     push @items, _new_item($self->order, $attr);
475   }
476   $self->order->add_items(@items);
477
478   $self->_recalc();
479
480   foreach my $item (@items) {
481     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
482     my $row_as_html = $self->p->render('order/tabs/_row',
483                                        ITEM              => $item,
484                                        ID                => $item_id,
485                                        ALL_PRICE_FACTORS => $self->all_price_factors
486     );
487
488     $self->js->append('#row_table_id', $row_as_html);
489   }
490
491   $self->js
492     ->run('kivi.Order.close_multi_items_dialog')
493     ->run('kivi.Order.init_row_handlers')
494     ->run('kivi.Order.row_table_scroll_down')
495     ->run('kivi.Order.renumber_positions')
496     ->focus('#add_item_parts_id_name');
497
498   $self->_js_redisplay_amounts_and_taxes;
499   $self->js->render();
500 }
501
502 # recalculate all linetotals, amounts and taxes and redisplay them
503 sub action_recalc_amounts_and_taxes {
504   my ($self) = @_;
505
506   $self->_recalc();
507
508   $self->_js_redisplay_linetotals;
509   $self->_js_redisplay_amounts_and_taxes;
510   $self->js->render();
511 }
512
513 # redisplay item rows if the are sorted by an attribute
514 sub action_reorder_items {
515   my ($self) = @_;
516
517   my %sort_keys = (
518     partnumber  => sub { $_[0]->part->partnumber },
519     description => sub { $_[0]->description },
520     qty         => sub { $_[0]->qty },
521     sellprice   => sub { $_[0]->sellprice },
522     discount    => sub { $_[0]->discount },
523   );
524
525   my $method = $sort_keys{$::form->{order_by}};
526   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
527   if ($::form->{sort_dir}) {
528     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
529   } else {
530     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
531   }
532   $self->js
533     ->run('kivi.Order.redisplay_items', \@to_sort)
534     ->render;
535 }
536
537 # show the popup to choose a price/discount source
538 sub action_price_popup {
539   my ($self) = @_;
540
541   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
542   my $item = $self->order->items_sorted->[$idx];
543
544   $self->render_price_dialog($item);
545 }
546
547 # get the longdescription for an item if the dialog to enter/change the
548 # longdescription was opened and the longdescription is empty
549 #
550 # If this item is new, get the longdescription from Part.
551 # Get it from OrderItem else.
552 sub action_get_item_longdescription {
553   my $longdescription;
554
555   if ($::form->{item_id}) {
556     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
557   } elsif ($::form->{parts_id}) {
558     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
559   }
560   $_[0]->render(\ $longdescription, { type => 'text' });
561 }
562
563 sub _js_redisplay_linetotals {
564   my ($self) = @_;
565
566   my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items_sorted };
567   $self->js
568     ->run('kivi.Order.redisplay_linetotals', \@data);
569 }
570
571 sub _js_redisplay_amounts_and_taxes {
572   my ($self) = @_;
573
574   if (scalar @{ $self->{taxes} }) {
575     $self->js->show('#taxincluded_row_id');
576   } else {
577     $self->js->hide('#taxincluded_row_id');
578   }
579
580   if ($self->order->taxincluded) {
581     $self->js->hide('#subtotal_row_id');
582   } else {
583     $self->js->show('#subtotal_row_id');
584   }
585
586   $self->js
587     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
588     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
589     ->remove('.tax_row')
590     ->insertBefore($self->build_tax_rows, '#amount_row_id');
591 }
592
593 #
594 # helpers
595 #
596
597 sub init_valid_types {
598   [ _sales_order_type(), _purchase_order_type() ];
599 }
600
601 sub init_type {
602   my ($self) = @_;
603
604   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
605     die "Not a valid type for order";
606   }
607
608   $self->type($::form->{type});
609 }
610
611 sub init_cv {
612   my ($self) = @_;
613
614   my $cv = $self->type eq _sales_order_type()    ? 'customer'
615          : $self->type eq _purchase_order_type() ? 'vendor'
616          : die "Not a valid type for order";
617
618   return $cv;
619 }
620
621 sub init_p {
622   SL::Presenter->get;
623 }
624
625 sub init_order {
626   $_[0]->_make_order;
627 }
628
629 # model used to filter/display the parts in the multi-items dialog
630 sub init_multi_items_models {
631   SL::Controller::Helper::GetModels->new(
632     controller     => $_[0],
633     model          => 'Part',
634     with_objects   => [ qw(unit_obj) ],
635     disable_plugin => 'paginated',
636     source         => $::form->{multi_items},
637     sorted         => {
638       _default    => {
639         by  => 'partnumber',
640         dir => 1,
641       },
642       partnumber  => t8('Partnumber'),
643       description => t8('Description')}
644   );
645 }
646
647 sub init_all_price_factors {
648   SL::DB::Manager::PriceFactor->get_all;
649 }
650
651 sub _check_auth {
652   my ($self) = @_;
653
654   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
655
656   my $right   = $right_for->{ $self->type };
657   $right    ||= 'DOES_NOT_EXIST';
658
659   $::auth->assert($right);
660 }
661
662 # build the selection box for contacts
663 #
664 # Needed, if customer/vendor changed.
665 sub build_contact_select {
666   my ($self) = @_;
667
668   $self->p->select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
669                        value_key  => 'cp_id',
670                        title_key  => 'full_name_dep',
671                        default    => $self->order->cp_id,
672                        with_empty => 1,
673                        style      => 'width: 300px',
674   );
675 }
676
677 # build the selection box for shiptos
678 #
679 # Needed, if customer/vendor changed.
680 sub build_shipto_select {
681   my ($self) = @_;
682
683   $self->p->select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
684                        value_key  => 'shipto_id',
685                        title_key  => 'displayable_id',
686                        default    => $self->order->shipto_id,
687                        with_empty => 1,
688                        style      => 'width: 300px',
689   );
690 }
691
692 # build the rows for displaying taxes
693 #
694 # Called if amounts where recalculated and redisplayed.
695 sub build_tax_rows {
696   my ($self) = @_;
697
698   my $rows_as_html;
699   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
700     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
701   }
702   return $rows_as_html;
703 }
704
705
706 sub render_price_dialog {
707   my ($self, $record_item) = @_;
708
709   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
710
711   $self->js
712     ->run(
713       'kivi.io.price_chooser_dialog',
714       t8('Available Prices'),
715       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
716     )
717     ->reinit_widgets;
718
719 #   if (@errors) {
720 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
721 #     $self->js->show('#dialog_flash_error');
722 #   }
723
724   $self->js->render;
725 }
726
727 sub _load_order {
728   my ($self) = @_;
729
730   return if !$::form->{id};
731
732   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
733 }
734
735 # load or create a new order object
736 #
737 # And assign changes from the for to this object.
738 # If the order is loaded from db, check if items are deleted in the form,
739 # remove them form the object and collect them for removing from db on saving.
740 # Then create/update items from form (via _make_item) and add them.
741 sub _make_order {
742   my ($self) = @_;
743
744   # add_items adds items to an order with no items for saving, but they cannot
745   # be retrieved via items until the order is saved. Adding empty items to new
746   # order here solves this problem.
747   my $order;
748   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
749   $order ||= SL::DB::Order->new(orderitems => []);
750
751   my $form_orderitems = delete $::form->{order}->{orderitems};
752   $order->assign_attributes(%{$::form->{order}});
753
754   # remove deleted items
755   $self->item_ids_to_delete([]);
756   foreach my $idx (reverse 0..$#{$order->orderitems}) {
757     my $item = $order->orderitems->[$idx];
758     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
759       splice @{$order->orderitems}, $idx, 1;
760       push @{$self->item_ids_to_delete}, $item->id;
761     }
762   }
763
764   my @items;
765   my $pos = 1;
766   foreach my $form_attr (@{$form_orderitems}) {
767     my $item = _make_item($order, $form_attr);
768     $item->position($pos);
769     push @items, $item;
770     $pos++;
771   }
772   $order->add_items(grep {!$_->id} @items);
773
774   return $order;
775 }
776
777 # create or update items from form
778 #
779 # Make item objects from form values. For items already existing read from db.
780 # Create a new item else. And assign attributes.
781 sub _make_item {
782   my ($record, $attr) = @_;
783
784   my $item;
785   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
786
787   my $is_new = !$item;
788
789   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
790   # they cannot be retrieved via custom_variables until the order/orderitem is
791   # saved. Adding empty custom_variables to new orderitem here solves this problem.
792   $item ||= SL::DB::OrderItem->new(custom_variables => []);
793
794   $item->assign_attributes(%$attr);
795   $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
796
797   return $item;
798 }
799
800 # create a new item
801 #
802 # This is used to add one (or more) items
803 sub _new_item {
804   my ($record, $attr) = @_;
805
806   my $item = SL::DB::OrderItem->new;
807   $item->assign_attributes(%$attr);
808
809   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
810   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
811
812   $item->unit($part->unit) if !$item->unit;
813
814   my $price_src;
815   if ($item->sellprice) {
816     $price_src = $price_source->price_from_source("");
817     $price_src->price($item->sellprice);
818   } else {
819     $price_src = $price_source->best_price
820            ? $price_source->best_price
821            : $price_source->price_from_source("");
822     $price_src->price(0) if !$price_source->best_price;
823   }
824
825   my $discount_src;
826   if ($item->discount) {
827     $discount_src = $price_source->discount_from_source("");
828     $discount_src->discount($item->discount);
829   } else {
830     $discount_src = $price_source->best_discount
831                   ? $price_source->best_discount
832                   : $price_source->discount_from_source("");
833     $discount_src->discount(0) if !$price_source->best_discount;
834   }
835
836   my %new_attr;
837   $new_attr{part}                   = $part;
838   $new_attr{description}            = $part->description     if ! $item->description;
839   $new_attr{qty}                    = 1.0                    if ! $item->qty;
840   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
841   $new_attr{sellprice}              = $price_src->price;
842   $new_attr{discount}               = $discount_src->discount;
843   $new_attr{active_price_source}    = $price_src;
844   $new_attr{active_discount_source} = $discount_src;
845
846   $new_attr{longdescription}        = $part->notes if ! defined $attr->{longdescription};
847
848   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
849   # they cannot be retrieved via custom_variables until the order/orderitem is
850   # saved. Adding empty custom_variables to new orderitem here solves this problem.
851   $new_attr{custom_variables} = [];
852
853   $item->assign_attributes(%new_attr);
854
855   return $item;
856 }
857
858 # recalculate prices and taxes
859 #
860 # Using the PriceTaxCalclulator. Store linetotals in the item objects.
861 sub _recalc {
862   my ($self) = @_;
863
864   # bb: todo: currency later
865   $self->order->currency_id($::instance_conf->get_currency_id());
866
867   my %pat = $self->order->calculate_prices_and_taxes();
868   $self->{taxes} = [];
869   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
870     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
871
872     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
873     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
874                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
875                                 tax       => $tax });
876   }
877
878   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
879 }
880
881 # get data for saving, printing, ..., that is not changed in the form
882 #
883 # Only cvars for now.
884 sub _get_unalterable_data {
885   my ($self) = @_;
886
887   foreach my $item (@{ $self->order->items }) {
888     # autovivify all cvars that are not in the form (cvars_by_config can do it).
889     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
890     foreach my $var (@{ $item->cvars_by_config }) {
891       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
892     }
893     $item->parse_custom_variable_values;
894   }
895 }
896
897 # delete the order
898 #
899 # And remove related files in the spool directory
900 sub _delete {
901   my ($self) = @_;
902
903   my $errors = [];
904   my $db = $self->order->db;
905
906   $db->do_transaction(
907     sub {
908       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
909       $self->order->delete;
910       my $spool = $::lx_office_conf{paths}->{spool};
911       unlink map { "$spool/$_" } @spoolfiles if $spool;
912
913       1;
914   }) || push(@{$errors}, $db->error);
915
916   return $errors;
917 }
918
919 # save the order
920 #
921 # And delete items that are deleted in the form.
922 sub _save {
923   my ($self) = @_;
924
925   my $errors = [];
926   my $db = $self->order->db;
927
928   $db->do_transaction(
929     sub {
930       SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
931       $self->order->save(cascade => 1);
932   }) || push(@{$errors}, $db->error);
933
934   return $errors;
935 }
936
937
938 sub _pre_render {
939   my ($self) = @_;
940
941   $self->{all_taxzones}        = SL::DB::Manager::TaxZone->get_all_sorted();
942   $self->{all_departments}     = SL::DB::Manager::Department->get_all_sorted();
943   $self->{all_employees}       = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
944                                                                                        deleted => 0 ] ],
945                                                                     sort_by => 'name');
946   $self->{all_salesmen}        = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
947                                                                                        deleted => 0 ] ],
948                                                                     sort_by => 'name');
949   $self->{all_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
950                                                                                       active => 1 ] ],
951                                                                    sort_by => 'projectnumber');
952   $self->{all_payment_terms}   = SL::DB::Manager::PaymentTerm->get_all_sorted();
953   $self->{all_delivery_terms}  = SL::DB::Manager::DeliveryTerm->get_all_sorted();
954
955   $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
956
957   my $print_form = Form->new('');
958   $print_form->{type}      = $self->type;
959   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
960   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
961   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
962     form => $print_form,
963     options => {dialog_name_prefix => 'print_options.',
964                 show_headers       => 1,
965                 no_queue           => 1,
966                 no_postscript      => 1,
967                 no_opendocument    => 1,
968                 no_html            => 1},
969   );
970
971   foreach my $item (@{$self->order->orderitems}) {
972     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
973     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
974     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
975   }
976
977   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
978     my $webdav = SL::Webdav->new(
979       type     => $self->type,
980       number   => $self->order->ordnumber,
981     );
982     my $webdav_path = $webdav->webdav_path;
983     my @all_objects = $webdav->get_all_objects;
984     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
985                                                     type => t8('File'),
986                                                     link => File::Spec->catdir($webdav_path, $_->filename),
987                                                 } } @all_objects;
988   }
989
990   $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
991 }
992
993 sub _create_pdf {
994   my ($order, $pdf_ref, $params) = @_;
995
996   my @errors = ();
997
998   my $print_form = Form->new('');
999   $print_form->{type}        = $order->type;
1000   $print_form->{formname}    = $params->{formname} || $order->type;
1001   $print_form->{format}      = $params->{format}   || 'pdf';
1002   $print_form->{media}       = $params->{media}    || 'file';
1003   $print_form->{groupitems}  = $params->{groupitems};
1004   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1005   $print_form->{language}    = $params->{language}->template_code if $print_form->{language};
1006   $print_form->{language_id} = $params->{language}->id            if $print_form->{language};
1007
1008   $order->flatten_to_form($print_form, format_amounts => 1);
1009
1010   # search for the template
1011   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1012     name        => $print_form->{formname},
1013     email       => $print_form->{media} eq 'email',
1014     language    => $params->{language},
1015     printer_id  => $print_form->{printer_id},  # todo
1016   );
1017
1018   if (!defined $template_file) {
1019     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);
1020   }
1021
1022   return @errors if scalar @errors;
1023
1024   $print_form->throw_on_error(sub {
1025     eval {
1026       $print_form->prepare_for_printing;
1027
1028       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1029         template  => $template_file,
1030         variables => $print_form,
1031         variable_content_types => {
1032           longdescription => 'html',
1033           partnotes       => 'html',
1034           notes           => 'html',
1035         },
1036       );
1037       1;
1038     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1039   });
1040
1041   return @errors;
1042 }
1043
1044 sub _sales_order_type {
1045   'sales_order';
1046 }
1047
1048 sub _purchase_order_type {
1049   'purchase_order';
1050 }
1051
1052 1;
1053
1054 __END__
1055
1056 =encoding utf-8
1057
1058 =head1 NAME
1059
1060 SL::Controller::Order - controller for orders
1061
1062 =head1 SYNOPSIS
1063
1064 This is a new form to enter orders, completely rewritten with the use
1065 of controller and java script techniques.
1066
1067 The aim is to provide the user a better expirience and a faster flow
1068 of work. Also the code should be more readable, more reliable and
1069 better to maintain.
1070
1071 =head2 key features
1072
1073 =over 2
1074
1075 =item *
1076 One input row, so that input happens every time at the same place.
1077
1078 =item *
1079 Use of pickers where possible.
1080
1081 =item *
1082 Possibility to enter more than one item at once.
1083
1084 =item *
1085 Save order only on "save" (and "save and delivery order"-workflow). No
1086 hidden save on "print" or "email". 
1087
1088 =item *
1089 Item list in a scrollable area, so that the workflow buttons stay at
1090 the bottom.
1091
1092 =item *
1093 Reordering item rows with drag and drop is possible. Sorting item rows is
1094 possible (by partnumber, description, qty, sellprice and discount for now).
1095
1096 =item *
1097 No "update" is necessary. All entries and calculations are managed
1098 with ajax-calls and the page does only reload on "save".
1099
1100 =item *
1101 User can see changes immediately, because of the use of java script
1102 and ajax.
1103
1104 =back
1105
1106 =head1 CODE
1107
1108 =head2 layout
1109
1110 =over 2
1111
1112 =item *
1113 SL/Controller/Order.pm: the controller
1114
1115 =item *
1116 template/webpages/order/form.html: main form
1117
1118 =item *
1119 template/webpages/order/tabs/basic_data.html: main tab for basic_data
1120
1121 This is the only tab here for now. "linked records" and "webdav" tabs are reused
1122 from generic code.
1123
1124 =over 3
1125
1126 =item *
1127 template/webpages/order/tabs/_item_input.html: the input line for items
1128
1129 =item *
1130 template/webpages/order/tabs/_row.html: one row for already entered items
1131
1132 =item *
1133 template/webpages/order/tabs/_tax_row.html: displaying tax information
1134
1135 =item *
1136 template/webpages/order/tabs/_multi_items_dialog.html: dialog for entering more
1137 than one item at once
1138
1139 =item *
1140 template/webpages/order/tabs/_multi_items_result.html: results for the filter in
1141 the multi items dialog
1142
1143 =item *
1144 template/webpages/order/tabs/_price_sources_dialog.html: dialog for selecting
1145 price and discount sources
1146
1147 =item *
1148 template/webpages/order/tabs/_email_dialog.html: email dialog
1149
1150 =back
1151
1152 =item *
1153 js/kivi.Order.js: java script functions
1154
1155 =back
1156
1157 =head1 TODO
1158
1159 =over 2
1160
1161 =item *
1162
1163 testing
1164
1165
1166 =item *
1167
1168 currency
1169
1170
1171 =item *
1172
1173 customer/vendor details ('D'-button)
1174
1175
1176 =item *
1177
1178 credit limit
1179
1180
1181 =item *
1182
1183 more workflows (save as new / invoice)
1184
1185
1186 =item *
1187
1188 price sources: little symbols showing better price / better discount
1189
1190
1191 =item *
1192
1193 custom shipto address
1194
1195
1196 =item *
1197
1198 periodic invoices
1199
1200
1201 =item *
1202
1203 more details on second row (marge, ...)
1204
1205
1206 =item *
1207
1208 language / part translations
1209
1210
1211 =item *
1212
1213 access rights
1214
1215
1216 =item *
1217
1218 preset salesman from customer
1219
1220
1221 =item *
1222
1223 display weights
1224
1225
1226 =item *
1227
1228 force project if enabled in client config
1229
1230
1231 =back
1232
1233 =head1 AUTHOR
1234
1235 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1236
1237 =cut