1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
8 use SL::Locale::String qw(t8);
9 use SL::SessionFile::Random;
20 use SL::Helper::CreatePDF qw(:all);
21 use SL::Helper::PrintOptions;
23 use SL::Controller::Helper::GetModels;
25 use List::Util qw(first);
26 use List::MoreUtils qw(none pairwise first_index);
27 use English qw(-no_match_vars);
30 use Rose::Object::MakeMethods::Generic
32 scalar => [ qw(item_ids_to_delete) ],
33 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
38 __PACKAGE__->run_before('_check_auth');
40 __PACKAGE__->run_before('_recalc',
41 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
43 __PACKAGE__->run_before('_get_unalterable_data',
44 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
54 $self->order->transdate(DateTime->now_local());
55 $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
60 title => $self->type eq _sales_order_type() ? $::locale->text('Add Sales Order')
61 : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
63 %{$self->{template_args}}
67 # edit an existing order
76 title => $self->type eq _sales_order_type() ? $::locale->text('Edit Sales Order')
77 : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
79 %{$self->{template_args}}
87 my $errors = $self->_delete();
89 if (scalar @{ $errors }) {
90 $self->js->flash('error', $_) foreach @{ $errors };
91 return $self->js->render();
94 flash_later('info', $::locale->text('The order has been deleted'));
95 my @redirect_params = (
100 $self->redirect_to(@redirect_params);
107 my $errors = $self->_save();
109 if (scalar @{ $errors }) {
110 $self->js->flash('error', $_) foreach @{ $errors };
111 return $self->js->render();
114 flash_later('info', $::locale->text('The order has been saved'));
115 my @redirect_params = (
118 id => $self->order->id,
121 $self->redirect_to(@redirect_params);
126 # This is called if "print" is pressed in the print dialog.
127 # If PDF creation was requested and succeeded, the pdf is stored in a session
128 # file and the filename is stored as session value with an unique key. A
129 # javascript function with this key is then called. This function calls the
130 # download action below (action_download_pdf), which offers the file for
135 my $format = $::form->{print_options}->{format};
136 my $media = $::form->{print_options}->{media};
137 my $formname = $::form->{print_options}->{formname};
138 my $copies = $::form->{print_options}->{copies};
139 my $groupitems = $::form->{print_options}->{groupitems};
142 if (none { $format eq $_ } qw(pdf)) {
143 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
146 # only screen or printer by now
147 if (none { $media eq $_ } qw(screen printer)) {
148 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
152 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
154 my $form = Form->new;
155 $form->{ordnumber} = $self->order->ordnumber;
156 $form->{type} = $self->type;
157 $form->{format} = $format;
158 $form->{formname} = $formname;
159 $form->{language} = '_' . $language->template_code if $language;
160 my $pdf_filename = $form->generate_attachment_filename();
163 my @errors = _create_pdf($self->order, \$pdf, { format => $format,
164 formname => $formname,
165 language => $language,
166 groupitems => $groupitems });
167 if (scalar @errors) {
168 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
171 if ($media eq 'screen') {
173 my $sfile = SL::SessionFile::Random->new(mode => "w");
174 $sfile->fh->print($pdf);
177 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
178 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
181 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
182 ->flash('info', t8('The PDF has been created'));
184 } elsif ($media eq 'printer') {
186 my $printer_id = $::form->{print_options}->{printer_id};
187 SL::DB::Printer->new(id => $printer_id)->load->print_document(
192 $self->js->flash('info', t8('The PDF has been printed'));
195 # copy file to webdav folder
196 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
197 my $webdav = SL::Webdav->new(
199 number => $self->order->ordnumber,
201 my $webdav_file = SL::Webdav::File->new(
203 filename => $pdf_filename,
206 $webdav_file->store(data => \$pdf);
209 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
216 # offer pdf for download
218 # It needs to get the key for the session value to get the pdf file.
219 sub action_download_pdf {
222 my $key = $::form->{key};
223 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
224 return $self->send_file(
226 type => 'application/pdf',
227 name => $::form->{pdf_filename},
231 # open the email dialog
232 sub action_show_email_dialog {
235 my $cv_method = $self->cv;
237 if (!$self->order->$cv_method) {
238 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'))
242 $self->{email}->{to} = $self->order->contact->cp_email if $self->order->contact;
243 $self->{email}->{to} ||= $self->order->$cv_method->email;
244 $self->{email}->{cc} = $self->order->$cv_method->cc;
245 $self->{email}->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
246 # Todo: get addresses from shipto, if any
248 my $form = Form->new;
249 $form->{ordnumber} = $self->order->ordnumber;
250 $form->{formname} = $self->type;
251 $form->{type} = $self->type;
252 $form->{language} = 'de';
253 $form->{format} = 'pdf';
255 $self->{email}->{subject} = $form->generate_email_subject();
256 $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
257 $self->{email}->{message} = $form->create_email_signature();
259 my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
261 ->run('kivi.Order.show_email_dialog', $dialog_html)
268 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
269 sub action_send_email {
272 my $mail = Mailer->new;
273 $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
274 $mail->{$_} = $::form->{email}->{$_} for qw(to cc bcc subject message);
277 my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
278 if (scalar @errors) {
279 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
282 $mail->{attachments} = [{ "content" => $pdf,
283 "name" => $::form->{email}->{attachment_filename} }];
285 if (my $err = $mail->send) {
286 return $self->js->flash('error', t8('Sending E-mail: ') . $err)
291 my $intnotes = $self->order->intnotes;
292 $intnotes .= "\n\n" if $self->order->intnotes;
293 $intnotes .= t8('[email]') . "\n";
294 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
295 $intnotes .= t8('To (email)') . ": " . $mail->{to} . "\n";
296 $intnotes .= t8('Cc') . ": " . $mail->{cc} . "\n" if $mail->{cc};
297 $intnotes .= t8('Bcc') . ": " . $mail->{bcc} . "\n" if $mail->{bcc};
298 $intnotes .= t8('Subject') . ": " . $mail->{subject} . "\n\n";
299 $intnotes .= t8('Message') . ": " . $mail->{message};
302 ->val('#order_intnotes', $intnotes)
303 ->run('kivi.Order.close_email_dialog')
307 # save the order and redirect to the frontend subroutine for a new
309 sub action_save_and_delivery_order {
312 my $errors = $self->_save();
314 if (scalar @{ $errors }) {
315 $self->js->flash('error', $_) foreach @{ $errors };
316 return $self->js->render();
318 flash_later('info', $::locale->text('The order has been saved'));
320 my @redirect_params = (
321 controller => 'oe.pl',
322 action => 'oe_delivery_order_from_order',
323 id => $self->order->id,
326 $self->redirect_to(@redirect_params);
329 # set form elements in respect of a changed customer or vendor
331 # This action is called on an change of the customer/vendor picker.
332 sub action_customer_vendor_changed {
335 my $cv_method = $self->cv;
337 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
338 $self->js->show('#cp_row');
340 $self->js->hide('#cp_row');
343 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
344 $self->js->show('#shipto_row');
346 $self->js->hide('#shipto_row');
349 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
351 if ($self->order->is_sales) {
352 $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
353 ? $self->order->$cv_method->taxincluded_checked
354 : $::myconfig{taxincluded_checked});
357 $self->order->payment_id($self->order->$cv_method->payment_id);
358 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
363 ->replaceWith('#order_cp_id', $self->build_contact_select)
364 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
365 ->val( '#order_taxzone_id', $self->order->taxzone_id)
366 ->val( '#order_taxincluded', $self->order->taxincluded)
367 ->val( '#order_payment_id', $self->order->payment_id)
368 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
369 ->val( '#order_intnotes', $self->order->$cv_method->notes)
370 ->focus( '#order_' . $self->cv . '_id');
372 $self->_js_redisplay_amounts_and_taxes;
376 # called if a unit in an existing item row is changed
377 sub action_unit_changed {
380 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
381 my $item = $self->order->items_sorted->[$idx];
383 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
384 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
389 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
390 $self->_js_redisplay_linetotals;
391 $self->_js_redisplay_amounts_and_taxes;
395 # add an item row for a new item entered in the input row
396 sub action_add_item {
399 my $form_attr = $::form->{add_item};
401 return unless $form_attr->{parts_id};
403 my $item = _new_item($self->order, $form_attr);
404 $self->order->add_items($item);
408 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
409 my $row_as_html = $self->p->render('order/tabs/_row',
412 ALL_PRICE_FACTORS => $self->all_price_factors
416 ->append('#row_table_id', $row_as_html)
417 ->val('.add_item_input', '')
418 ->run('kivi.Order.init_row_handlers')
419 ->run('kivi.Order.row_table_scroll_down')
420 ->run('kivi.Order.renumber_positions')
421 ->focus('#add_item_parts_id_name');
423 $self->_js_redisplay_amounts_and_taxes;
427 # open the dialog for entering multiple items at once
428 sub action_show_multi_items_dialog {
429 require SL::DB::PartsGroup;
430 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
431 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
434 # update the filter results in the multi item dialog
435 sub action_multi_items_update_result {
438 $::form->{multi_items}->{filter}->{obsolete} = 0;
440 my $count = $_[0]->multi_items_models->count;
443 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
444 $_[0]->render($text, { layout => 0 });
445 } elsif ($count > $max_count) {
446 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
447 $_[0]->render($text, { layout => 0 });
449 my $multi_items = $_[0]->multi_items_models->get;
450 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
451 multi_items => $multi_items);
455 # add item rows for multiple items add once
456 sub action_add_multi_items {
459 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
460 return $self->js->render() unless scalar @form_attr;
463 foreach my $attr (@form_attr) {
464 push @items, _new_item($self->order, $attr);
466 $self->order->add_items(@items);
470 foreach my $item (@items) {
471 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
472 my $row_as_html = $self->p->render('order/tabs/_row',
475 ALL_PRICE_FACTORS => $self->all_price_factors
478 $self->js->append('#row_table_id', $row_as_html);
482 ->run('kivi.Order.close_multi_items_dialog')
483 ->run('kivi.Order.init_row_handlers')
484 ->run('kivi.Order.row_table_scroll_down')
485 ->run('kivi.Order.renumber_positions')
486 ->focus('#add_item_parts_id_name');
488 $self->_js_redisplay_amounts_and_taxes;
492 # recalculate all linetotals, amounts and taxes and redisplay them
493 sub action_recalc_amounts_and_taxes {
498 $self->_js_redisplay_linetotals;
499 $self->_js_redisplay_amounts_and_taxes;
503 # redisplay item rows if the are sorted by an attribute
504 sub action_reorder_items {
508 partnumber => sub { $_[0]->part->partnumber },
509 description => sub { $_[0]->description },
510 qty => sub { $_[0]->qty },
511 sellprice => sub { $_[0]->sellprice },
512 discount => sub { $_[0]->discount },
515 my $method = $sort_keys{$::form->{order_by}};
516 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
517 if ($::form->{sort_dir}) {
518 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
520 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
523 ->run('kivi.Order.redisplay_items', \@to_sort)
527 # show the popup to choose a price/discount source
528 sub action_price_popup {
531 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
532 my $item = $self->order->items_sorted->[$idx];
534 $self->render_price_dialog($item);
537 # get the longdescription for an item if the dialog to enter/change the
538 # longdescription was opened and the longdescription is empty
540 # If this item is new, get the longdescription from Part.
541 # Get it from OrderItem else.
542 sub action_get_item_longdescription {
545 if ($::form->{item_id}) {
546 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
547 } elsif ($::form->{parts_id}) {
548 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
550 $_[0]->render(\ $longdescription, { type => 'text' });
553 sub _js_redisplay_linetotals {
556 my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items_sorted };
558 ->run('kivi.Order.redisplay_linetotals', \@data);
561 sub _js_redisplay_amounts_and_taxes {
564 if (scalar @{ $self->{taxes} }) {
565 $self->js->show('#taxincluded_row_id');
567 $self->js->hide('#taxincluded_row_id');
570 if ($self->order->taxincluded) {
571 $self->js->hide('#subtotal_row_id');
573 $self->js->show('#subtotal_row_id');
577 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
578 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
580 ->insertBefore($self->build_tax_rows, '#amount_row_id');
587 sub init_valid_types {
588 [ _sales_order_type(), _purchase_order_type() ];
594 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
595 die "Not a valid type for order";
598 $self->type($::form->{type});
604 my $cv = $self->type eq _sales_order_type() ? 'customer'
605 : $self->type eq _purchase_order_type() ? 'vendor'
606 : die "Not a valid type for order";
619 # model used to filter/display the parts in the multi-items dialog
620 sub init_multi_items_models {
621 SL::Controller::Helper::GetModels->new(
624 with_objects => [ qw(unit_obj) ],
625 disable_plugin => 'paginated',
626 source => $::form->{multi_items},
632 partnumber => t8('Partnumber'),
633 description => t8('Description')}
637 sub init_all_price_factors {
638 SL::DB::Manager::PriceFactor->get_all;
644 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
646 my $right = $right_for->{ $self->type };
647 $right ||= 'DOES_NOT_EXIST';
649 $::auth->assert($right);
652 # build the selection box for contacts
654 # Needed, if customer/vendor changed.
655 sub build_contact_select {
658 $self->p->select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
659 value_key => 'cp_id',
660 title_key => 'full_name_dep',
661 default => $self->order->cp_id,
663 style => 'width: 300px',
667 # build the selection box for shiptos
669 # Needed, if customer/vendor changed.
670 sub build_shipto_select {
673 $self->p->select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
674 value_key => 'shipto_id',
675 title_key => 'displayable_id',
676 default => $self->order->shipto_id,
678 style => 'width: 300px',
682 # build the rows for displaying taxes
684 # Called if amounts where recalculated and redisplayed.
689 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
690 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
692 return $rows_as_html;
696 sub render_price_dialog {
697 my ($self, $record_item) = @_;
699 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
703 'kivi.io.price_chooser_dialog',
704 t8('Available Prices'),
705 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
710 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
711 # $self->js->show('#dialog_flash_error');
720 return if !$::form->{id};
722 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
725 # load or create a new order object
727 # And assign changes from the for to this object.
728 # If the order is loaded from db, check if items are deleted in the form,
729 # remove them form the object and collect them for removing from db on saving.
730 # Then create/update items from form (via _make_item) and add them.
734 # add_items adds items to an order with no items for saving, but they cannot
735 # be retrieved via items until the order is saved. Adding empty items to new
736 # order here solves this problem.
738 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
739 $order ||= SL::DB::Order->new(orderitems => []);
741 my $form_orderitems = delete $::form->{order}->{orderitems};
742 $order->assign_attributes(%{$::form->{order}});
744 # remove deleted items
745 $self->item_ids_to_delete([]);
746 foreach my $idx (reverse 0..$#{$order->orderitems}) {
747 my $item = $order->orderitems->[$idx];
748 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
749 splice @{$order->orderitems}, $idx, 1;
750 push @{$self->item_ids_to_delete}, $item->id;
756 foreach my $form_attr (@{$form_orderitems}) {
757 my $item = _make_item($order, $form_attr);
758 $item->position($pos);
762 $order->add_items(grep {!$_->id} @items);
767 # create or update items from form
769 # Make item objects from form values. For items already existing read from db.
770 # Create a new item else. And assign attributes.
772 my ($record, $attr) = @_;
775 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
779 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
780 # they cannot be retrieved via custom_variables until the order/orderitem is
781 # saved. Adding empty custom_variables to new orderitem here solves this problem.
782 $item ||= SL::DB::OrderItem->new(custom_variables => []);
784 $item->assign_attributes(%$attr);
785 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
792 # This is used to add one (or more) items
794 my ($record, $attr) = @_;
796 my $item = SL::DB::OrderItem->new;
797 $item->assign_attributes(%$attr);
799 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
800 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
802 $item->unit($part->unit) if !$item->unit;
805 if ($item->sellprice) {
806 $price_src = $price_source->price_from_source("");
807 $price_src->price($item->sellprice);
809 $price_src = $price_source->best_price
810 ? $price_source->best_price
811 : $price_source->price_from_source("");
812 $price_src->price(0) if !$price_source->best_price;
816 if ($item->discount) {
817 $discount_src = $price_source->discount_from_source("");
818 $discount_src->discount($item->discount);
820 $discount_src = $price_source->best_discount
821 ? $price_source->best_discount
822 : $price_source->discount_from_source("");
823 $discount_src->discount(0) if !$price_source->best_discount;
827 $new_attr{part} = $part;
828 $new_attr{description} = $part->description if ! $item->description;
829 $new_attr{qty} = 1.0 if ! $item->qty;
830 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
831 $new_attr{sellprice} = $price_src->price;
832 $new_attr{discount} = $discount_src->discount;
833 $new_attr{active_price_source} = $price_src;
834 $new_attr{active_discount_source} = $discount_src;
836 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
838 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
839 # they cannot be retrieved via custom_variables until the order/orderitem is
840 # saved. Adding empty custom_variables to new orderitem here solves this problem.
841 $new_attr{custom_variables} = [];
843 $item->assign_attributes(%new_attr);
848 # recalculate prices and taxes
850 # Using the PriceTaxCalculator. Store linetotals in the item objects.
854 # bb: todo: currency later
855 $self->order->currency_id($::instance_conf->get_currency_id());
857 my %pat = $self->order->calculate_prices_and_taxes();
859 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
860 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
862 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
863 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
864 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
868 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
871 # get data for saving, printing, ..., that is not changed in the form
873 # Only cvars for now.
874 sub _get_unalterable_data {
877 foreach my $item (@{ $self->order->items }) {
878 # autovivify all cvars that are not in the form (cvars_by_config can do it).
879 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
880 foreach my $var (@{ $item->cvars_by_config }) {
881 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
883 $item->parse_custom_variable_values;
889 # And remove related files in the spool directory
894 my $db = $self->order->db;
898 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
899 $self->order->delete;
900 my $spool = $::lx_office_conf{paths}->{spool};
901 unlink map { "$spool/$_" } @spoolfiles if $spool;
904 }) || push(@{$errors}, $db->error);
911 # And delete items that are deleted in the form.
916 my $db = $self->order->db;
920 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
921 $self->order->save(cascade => 1);
922 }) || push(@{$errors}, $db->error);
931 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
932 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
933 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
936 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
939 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
941 sort_by => 'projectnumber');
942 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted();
943 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
945 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
947 my $print_form = Form->new('');
948 $print_form->{type} = $self->type;
949 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
950 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
951 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
953 options => {dialog_name_prefix => 'print_options.',
957 no_opendocument => 1,
961 foreach my $item (@{$self->order->orderitems}) {
962 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
963 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
964 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
967 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
968 my $webdav = SL::Webdav->new(
970 number => $self->order->ordnumber,
972 my $webdav_path = $webdav->webdav_path;
973 my @all_objects = $webdav->get_all_objects;
974 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
976 link => File::Spec->catdir($webdav_path, $_->filename),
980 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
984 my ($order, $pdf_ref, $params) = @_;
988 my $print_form = Form->new('');
989 $print_form->{type} = $order->type;
990 $print_form->{formname} = $params->{formname} || $order->type;
991 $print_form->{format} = $params->{format} || 'pdf';
992 $print_form->{media} = $params->{media} || 'file';
993 $print_form->{groupitems} = $params->{groupitems};
994 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
995 $print_form->{language} = $params->{language}->template_code if $print_form->{language};
996 $print_form->{language_id} = $params->{language}->id if $print_form->{language};
998 $order->flatten_to_form($print_form, format_amounts => 1);
1000 # search for the template
1001 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1002 name => $print_form->{formname},
1003 email => $print_form->{media} eq 'email',
1004 language => $params->{language},
1005 printer_id => $print_form->{printer_id}, # todo
1008 if (!defined $template_file) {
1009 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);
1012 return @errors if scalar @errors;
1014 $print_form->throw_on_error(sub {
1016 $print_form->prepare_for_printing;
1018 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1019 template => $template_file,
1020 variables => $print_form,
1021 variable_content_types => {
1022 longdescription => 'html',
1023 partnotes => 'html',
1028 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1034 sub _sales_order_type {
1038 sub _purchase_order_type {
1050 SL::Controller::Order - controller for orders
1054 This is a new form to enter orders, completely rewritten with the use
1055 of controller and java script techniques.
1057 The aim is to provide the user a better expirience and a faster flow
1058 of work. Also the code should be more readable, more reliable and
1067 One input row, so that input happens every time at the same place.
1071 Use of pickers where possible.
1075 Possibility to enter more than one item at once.
1079 Save order only on "save" (and "save and delivery order"-workflow). No
1080 hidden save on "print" or "email".
1084 Item list in a scrollable area, so that the workflow buttons stay at
1089 Reordering item rows with drag and drop is possible. Sorting item rows is
1090 possible (by partnumber, description, qty, sellprice and discount for now).
1094 No C<update> is necessary. All entries and calculations are managed
1095 with ajax-calls and the page does only reload on C<save>.
1099 User can see changes immediately, because of the use of java script
1110 =item * C<SL/Controller/Order.pm>
1114 =item * C<template/webpages/order/form.html>
1118 =item * C<template/webpages/order/tabs/basic_data.html>
1120 Main tab for basic_data.
1122 This is the only tab here for now. "linked records" and "webdav" tabs are
1123 reused from generic code.
1127 =item * C<template/webpages/order/tabs/_item_input.html>
1129 The input line for items
1131 =item * C<template/webpages/order/tabs/_row.html>
1133 One row for already entered items
1135 =item * C<template/webpages/order/tabs/_tax_row.html>
1137 Displaying tax information
1139 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1141 Dialog for entering more than one item at once
1143 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1145 Results for the filter in the multi items dialog
1147 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1149 Dialog for selecting price and discount sources
1151 =item * C<template/webpages/order/tabs/_email_dialog.html>
1157 =item * C<js/kivi.Order.js>
1159 java script functions
1171 =item * customer/vendor details ('D'-button)
1173 =item * credit limit
1175 =item * more workflows (save as new / invoice)
1177 =item * price sources: little symbols showing better price / better discount
1179 =item * custom shipto address
1181 =item * periodic invoices
1183 =item * more details on second row (marge, ...)
1185 =item * language / part translations
1187 =item * access rights
1189 =item * preset salesman from customer
1191 =item * display weights
1193 =item * force project if enabled in client config
1201 =head1 KNOWN BUGS AND CAVEATS
1207 Customer discount is not displayed as a valid discount in price source popup
1208 (this might be a bug in price sources)
1212 No indication that double click expands second row, no exand all button
1216 Implementation of second row with a tbody for every item is not supported by
1221 As a consequence row striping does not currently work
1225 Inline creation of parts is not currently supported
1229 Table header is not sticky in the scrolling area.
1233 Sorting does not include C<position>, neither does reordering.
1235 This behavior was implemented intentionally. But we can discuss, which behavior
1236 should be implemented.
1240 C<show_multi_items_dialog> does not use the currently inserted string for
1245 Rendering a 50 items order takes twice as long as the old code.
1247 90% of that is rendering the (hidden) second rows, and 50% of those again are
1248 checks for is_valid and C<INCLUDE> on the cvar input template.
1250 Suggestion: fetch second rows when asked for.
1256 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>