1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
8 use SL::Locale::String;
9 use SL::SessionFile::Random;
24 use SL::DB::PriceFactor;
29 use SL::Helper::DateTime;
30 use SL::Helper::CreatePDF qw(:all);
31 use SL::Helper::PrintOptions;
33 use SL::Controller::Helper::GetModels;
35 use List::Util qw(max first);
36 use List::MoreUtils qw(none pairwise first_index);
37 use English qw(-no_match_vars);
40 use Rose::Object::MakeMethods::Generic
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) ],
48 __PACKAGE__->run_before('_check_auth');
50 __PACKAGE__->run_before('_recalc',
51 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
53 __PACKAGE__->run_before('_get_unalterable_data',
54 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
64 $self->order->transdate(DateTime->now_local());
65 $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
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')
73 %{$self->{template_args}}
77 # edit an existing order
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')
89 %{$self->{template_args}}
97 my $errors = $self->_delete();
99 if (scalar @{ $errors }) {
100 $self->js->flash('error', $_) foreach @{ $errors };
101 return $self->js->render();
104 flash_later('info', $::locale->text('The order has been deleted'));
105 my @redirect_params = (
110 $self->redirect_to(@redirect_params);
117 my $errors = $self->_save();
119 if (scalar @{ $errors }) {
120 $self->js->flash('error', $_) foreach @{ $errors };
121 return $self->js->render();
124 flash_later('info', $::locale->text('The order has been saved'));
125 my @redirect_params = (
128 id => $self->order->id,
131 $self->redirect_to(@redirect_params);
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
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};
152 if (none { $format eq $_ } qw(pdf)) {
153 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
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;
162 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
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();
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;
181 if ($media eq 'screen') {
183 my $sfile = SL::SessionFile::Random->new(mode => "w");
184 $sfile->fh->print($pdf);
187 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
188 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
191 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
192 ->flash('info', t8('The PDF has been created'));
194 } elsif ($media eq 'printer') {
196 my $printer_id = $::form->{print_options}->{printer_id};
197 SL::DB::Printer->new(id => $printer_id)->load->print_document(
202 $self->js->flash('info', t8('The PDF has been printed'));
205 # copy file to webdav folder
206 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
207 my $webdav = SL::Webdav->new(
209 number => $self->order->ordnumber,
211 my $webdav_file = SL::Webdav::File->new(
213 filename => $pdf_filename,
216 $webdav_file->store(data => \$pdf);
219 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
226 # offer pdf for download
228 # It needs to get the key for the session value to get the pdf file.
229 sub action_download_pdf {
232 my $key = $::form->{key};
233 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
234 return $self->send_file(
236 type => 'application/pdf',
237 name => $::form->{pdf_filename},
241 # open the email dialog
242 sub action_show_email_dialog {
245 my $cv_method = $self->cv;
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'))
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
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';
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();
269 my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
271 ->run('kivi.Order.show_email_dialog', $dialog_html)
278 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
279 sub action_send_email {
282 my $mail = Mailer->new;
283 $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
284 $mail->{$_} = $::form->{email}->{$_} for qw(to cc bcc subject message);
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);
292 $mail->{attachments} = [{ "content" => $pdf,
293 "name" => $::form->{email}->{attachment_filename} }];
295 if (my $err = $mail->send) {
296 return $self->js->flash('error', t8('Sending E-mail: ') . $err)
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};
312 ->val('#order_intnotes', $intnotes)
313 ->run('kivi.Order.close_email_dialog')
317 # save the order and redirect to the frontend subroutine for a new
319 sub action_save_and_delivery_order {
322 my $errors = $self->_save();
324 if (scalar @{ $errors }) {
325 $self->js->flash('error', $_) foreach @{ $errors };
326 return $self->js->render();
328 flash_later('info', $::locale->text('The order has been saved'));
330 my @redirect_params = (
331 controller => 'oe.pl',
332 action => 'oe_delivery_order_from_order',
333 id => $self->order->id,
336 $self->redirect_to(@redirect_params);
339 # set form elements in respect of a changed customer or vendor
341 # This action is called on an change of the customer/vendor picker.
342 sub action_customer_vendor_changed {
345 my $cv_method = $self->cv;
347 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
348 $self->js->show('#cp_row');
350 $self->js->hide('#cp_row');
353 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
354 $self->js->show('#shipto_row');
356 $self->js->hide('#shipto_row');
359 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
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});
367 $self->order->payment_id($self->order->$cv_method->payment_id);
368 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
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');
382 $self->_js_redisplay_amounts_and_taxes;
386 # called if a unit in an existing item row is changed
387 sub action_unit_changed {
390 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
391 my $item = $self->order->items_sorted->[$idx];
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));
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;
405 # add an item row for a new item entered in the input row
406 sub action_add_item {
409 my $form_attr = $::form->{add_item};
411 return unless $form_attr->{parts_id};
413 my $item = _new_item($self->order, $form_attr);
414 $self->order->add_items($item);
418 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
419 my $row_as_html = $self->p->render('order/tabs/_row',
422 ALL_PRICE_FACTORS => $self->all_price_factors
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');
433 $self->_js_redisplay_amounts_and_taxes;
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);
444 # update the filter results in the multi item dialog
445 sub action_multi_items_update_result {
448 $::form->{multi_items}->{filter}->{obsolete} = 0;
450 my $count = $_[0]->multi_items_models->count;
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 });
459 my $multi_items = $_[0]->multi_items_models->get;
460 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
461 multi_items => $multi_items);
465 # add item rows for multiple items add once
466 sub action_add_multi_items {
469 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
470 return $self->js->render() unless scalar @form_attr;
473 foreach my $attr (@form_attr) {
474 push @items, _new_item($self->order, $attr);
476 $self->order->add_items(@items);
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',
485 ALL_PRICE_FACTORS => $self->all_price_factors
488 $self->js->append('#row_table_id', $row_as_html);
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');
498 $self->_js_redisplay_amounts_and_taxes;
502 # recalculate all linetotals, amounts and taxes and redisplay them
503 sub action_recalc_amounts_and_taxes {
508 $self->_js_redisplay_linetotals;
509 $self->_js_redisplay_amounts_and_taxes;
513 # redisplay item rows if the are sorted by an attribute
514 sub action_reorder_items {
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 },
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;
530 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
533 ->run('kivi.Order.redisplay_items', \@to_sort)
537 # show the popup to choose a price/discount source
538 sub action_price_popup {
541 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
542 my $item = $self->order->items_sorted->[$idx];
544 $self->render_price_dialog($item);
547 # get the longdescription for an item if the dialog to enter/change the
548 # longdescription was opened and the longdescription is empty
550 # If this item is new, get the longdescription from Part.
551 # Get it from OrderItem else.
552 sub action_get_item_longdescription {
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;
560 $_[0]->render(\ $longdescription, { type => 'text' });
563 sub _js_redisplay_linetotals {
566 my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items_sorted };
568 ->run('kivi.Order.redisplay_linetotals', \@data);
571 sub _js_redisplay_amounts_and_taxes {
574 if (scalar @{ $self->{taxes} }) {
575 $self->js->show('#taxincluded_row_id');
577 $self->js->hide('#taxincluded_row_id');
580 if ($self->order->taxincluded) {
581 $self->js->hide('#subtotal_row_id');
583 $self->js->show('#subtotal_row_id');
587 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
588 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
590 ->insertBefore($self->build_tax_rows, '#amount_row_id');
597 sub init_valid_types {
598 [ _sales_order_type(), _purchase_order_type() ];
604 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
605 die "Not a valid type for order";
608 $self->type($::form->{type});
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";
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(
634 with_objects => [ qw(unit_obj) ],
635 disable_plugin => 'paginated',
636 source => $::form->{multi_items},
642 partnumber => t8('Partnumber'),
643 description => t8('Description')}
647 sub init_all_price_factors {
648 SL::DB::Manager::PriceFactor->get_all;
654 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
656 my $right = $right_for->{ $self->type };
657 $right ||= 'DOES_NOT_EXIST';
659 $::auth->assert($right);
662 # build the selection box for contacts
664 # Needed, if customer/vendor changed.
665 sub build_contact_select {
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,
673 style => 'width: 300px',
677 # build the selection box for shiptos
679 # Needed, if customer/vendor changed.
680 sub build_shipto_select {
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,
688 style => 'width: 300px',
692 # build the rows for displaying taxes
694 # Called if amounts where recalculated and redisplayed.
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);
702 return $rows_as_html;
706 sub render_price_dialog {
707 my ($self, $record_item) = @_;
709 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
713 'kivi.io.price_chooser_dialog',
714 t8('Available Prices'),
715 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
720 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
721 # $self->js->show('#dialog_flash_error');
730 return if !$::form->{id};
732 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
735 # load or create a new order object
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.
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.
748 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
749 $order ||= SL::DB::Order->new(orderitems => []);
751 my $form_orderitems = delete $::form->{order}->{orderitems};
752 $order->assign_attributes(%{$::form->{order}});
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;
766 foreach my $form_attr (@{$form_orderitems}) {
767 my $item = _make_item($order, $form_attr);
768 $item->position($pos);
772 $order->add_items(grep {!$_->id} @items);
777 # create or update items from form
779 # Make item objects from form values. For items already existing read from db.
780 # Create a new item else. And assign attributes.
782 my ($record, $attr) = @_;
785 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
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 => []);
794 $item->assign_attributes(%$attr);
795 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
802 # This is used to add one (or more) items
804 my ($record, $attr) = @_;
806 my $item = SL::DB::OrderItem->new;
807 $item->assign_attributes(%$attr);
809 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
810 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
812 $item->unit($part->unit) if !$item->unit;
815 if ($item->sellprice) {
816 $price_src = $price_source->price_from_source("");
817 $price_src->price($item->sellprice);
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;
826 if ($item->discount) {
827 $discount_src = $price_source->discount_from_source("");
828 $discount_src->discount($item->discount);
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;
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;
846 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
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} = [];
853 $item->assign_attributes(%new_attr);
858 # recalculate prices and taxes
860 # Using the PriceTaxCalclulator. Store linetotals in the item objects.
864 # bb: todo: currency later
865 $self->order->currency_id($::instance_conf->get_currency_id());
867 my %pat = $self->order->calculate_prices_and_taxes();
869 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
870 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
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},
878 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
881 # get data for saving, printing, ..., that is not changed in the form
883 # Only cvars for now.
884 sub _get_unalterable_data {
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}));
893 $item->parse_custom_variable_values;
899 # And remove related files in the spool directory
904 my $db = $self->order->db;
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;
914 }) || push(@{$errors}, $db->error);
921 # And delete items that are deleted in the form.
926 my $db = $self->order->db;
930 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
931 $self->order->save(cascade => 1);
932 }) || push(@{$errors}, $db->error);
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,
946 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
949 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
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();
955 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
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(
963 options => {dialog_name_prefix => 'print_options.',
967 no_opendocument => 1,
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));
977 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
978 my $webdav = SL::Webdav->new(
980 number => $self->order->ordnumber,
982 my $webdav_path = $webdav->webdav_path;
983 my @all_objects = $webdav->get_all_objects;
984 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
986 link => File::Spec->catdir($webdav_path, $_->filename),
990 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
994 my ($order, $pdf_ref, $params) = @_;
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};
1008 $order->flatten_to_form($print_form, format_amounts => 1);
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
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);
1022 return @errors if scalar @errors;
1024 $print_form->throw_on_error(sub {
1026 $print_form->prepare_for_printing;
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',
1038 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1044 sub _sales_order_type {
1048 sub _purchase_order_type {
1060 SL::Controller::Order - controller for orders
1064 This is a new form to enter orders, completely rewritten with the use
1065 of controller and java script techniques.
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
1076 One input row, so that input happens every time at the same place.
1079 Use of pickers where possible.
1082 Possibility to enter more than one item at once.
1085 Save order only on "save" (and "save and delivery order"-workflow). No
1086 hidden save on "print" or "email".
1089 Item list in a scrollable area, so that the workflow buttons stay at
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).
1097 No "update" is necessary. All entries and calculations are managed
1098 with ajax-calls and the page does only reload on "save".
1101 User can see changes immediately, because of the use of java script
1113 SL/Controller/Order.pm: the controller
1116 template/webpages/order/form.html: main form
1119 template/webpages/order/tabs/basic_data.html: main tab for basic_data
1121 This is the only tab here for now. "linked records" and "webdav" tabs are reused
1127 template/webpages/order/tabs/_item_input.html: the input line for items
1130 template/webpages/order/tabs/_row.html: one row for already entered items
1133 template/webpages/order/tabs/_tax_row.html: displaying tax information
1136 template/webpages/order/tabs/_multi_items_dialog.html: dialog for entering more
1137 than one item at once
1140 template/webpages/order/tabs/_multi_items_result.html: results for the filter in
1141 the multi items dialog
1144 template/webpages/order/tabs/_price_sources_dialog.html: dialog for selecting
1145 price and discount sources
1148 template/webpages/order/tabs/_email_dialog.html: email dialog
1153 js/kivi.Order.js: java script functions
1173 customer/vendor details ('D'-button)
1183 more workflows (save as new / invoice)
1188 price sources: little symbols showing better price / better discount
1193 custom shipto address
1203 more details on second row (marge, ...)
1208 language / part translations
1218 preset salesman from customer
1228 force project if enabled in client config
1235 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>