X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FController%2FOrder.pm;h=46e31c336ee977e0a1df4ca8fa797e6b23cfaeb2;hb=44ed4e99507c6de660029eabad6b3ccdc7ab5fd6;hp=9c1d77366a4a92c8ec8ce7ef821d64afac9abbb8;hpb=bba88a1b49d0c06c0b496dedc41599712a12c8e3;p=kivitendo-erp.git diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm index 9c1d77366..46e31c336 100644 --- a/SL/Controller/Order.pm +++ b/SL/Controller/Order.pm @@ -11,6 +11,7 @@ use SL::PriceSource; use SL::Webdav; use SL::File; use SL::Util qw(trim); +use SL::YAML; use SL::DB::Order; use SL::DB::Default; use SL::DB::Unit; @@ -22,6 +23,7 @@ use SL::DB::RecordLink; use SL::Helper::CreatePDF qw(:all); use SL::Helper::PrintOptions; +use SL::Helper::ShippedQty; use SL::Controller::Helper::GetModels; @@ -31,6 +33,7 @@ use List::MoreUtils qw(any none pairwise first_index); use English qw(-no_match_vars); use File::Spec; use Cwd; +use Sort::Naturally; use Rose::Object::MakeMethods::Generic ( @@ -57,9 +60,11 @@ sub action_add { my ($self) = @_; $self->order->transdate(DateTime->now_local()); - my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1; + my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval : + $self->{type} eq 'sales_order' ? $::instance_conf->get_delivery_date_interval : 1; $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate; + $self->pre_render(); $self->render( 'order/form', @@ -72,7 +77,23 @@ sub action_add { sub action_edit { my ($self) = @_; - $self->load_order; + if ($::form->{id}) { + $self->load_order; + + } else { + # this is to edit an order from an unsaved order object + + # set item ids to new fake id, to identify them as new items + foreach my $item (@{$self->order->items_sorted}) { + $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000); + } + # trigger rendering values for second row/longdescription as hidden, + # because they are loaded only on demand. So we need to keep the values + # from the source. + $_->{render_second_row} = 1 for @{ $self->order->items_sorted }; + $_->{render_longdescription} = 1 for @{ $self->order->items_sorted }; + } + $self->recalc(); $self->pre_render(); $self->render( @@ -82,6 +103,36 @@ sub action_edit { ); } +# edit a collective order (consisting of one or more existing orders) +sub action_edit_collective { + my ($self) = @_; + + # collect order ids + my @multi_ids = map { + $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1} + } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form; + + # fall back to add if no ids are given + if (scalar @multi_ids == 0) { + $self->action_add(); + return; + } + + # fall back to save as new if only one id is given + if (scalar @multi_ids == 1) { + $self->order(SL::DB::Order->new(id => $multi_ids[0])->load); + $self->action_save_as_new(); + return; + } + + # make new order from given orders + my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids; + $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders; + $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate')); + + $self->action_edit(); +} + # delete the order sub action_delete { my ($self) = @_; @@ -163,7 +214,8 @@ sub action_save_as_new { # Set new reqdate unless changed if ($order->reqdate == $saved_order->reqdate) { - my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1; + my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval : + $self->{type} eq 'sales_order' ? $::instance_conf->get_delivery_date_interval : 1; $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days); } else { $new_attrs{reqdate} = $order->reqdate; @@ -193,14 +245,23 @@ sub action_save_as_new { sub action_print { my ($self) = @_; + my $errors = $self->save(); + + if (scalar @{ $errors }) { + $self->js->flash('error', $_) foreach @{ $errors }; + return $self->js->render(); + } + + $self->js_reset_order_and_item_ids_after_save; + my $format = $::form->{print_options}->{format}; my $media = $::form->{print_options}->{media}; my $formname = $::form->{print_options}->{formname}; my $copies = $::form->{print_options}->{copies}; my $groupitems = $::form->{print_options}->{groupitems}; - # only pdf by now - if (none { $format eq $_ } qw(pdf)) { + # only pdf and opendocument by now + if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) { return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render; } @@ -353,6 +414,16 @@ sub action_show_email_dialog { sub action_send_email { my ($self) = @_; + my $errors = $self->save(); + + if (scalar @{ $errors }) { + $self->js->run('kivi.Order.close_email_dialog'); + $self->js->flash('error', $_) foreach @{ $errors }; + return $self->js->render(); + } + + $self->js_reset_order_and_item_ids_after_save; + my $email_form = delete $::form->{email_form}; my %field_names = (to => 'email'); @@ -370,11 +441,11 @@ sub action_send_email { $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id}; my $pdf; - my @errors = genereate_pdf($self->order, \$pdf, {media => $::form->{media}, - format => $::form->{print_options}->{format}, - formname => $::form->{print_options}->{formname}, - language => $language, - groupitems => $::form->{print_options}->{groupitems}}); + my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media}, + format => $::form->{print_options}->{format}, + formname => $::form->{print_options}->{formname}, + language => $language, + groupitems => $::form->{print_options}->{groupitems}}); if (scalar @errors) { return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self); } @@ -400,6 +471,8 @@ sub action_send_email { $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n"; $intnotes .= t8('Message') . ": " . $::form->{message}; + $self->order->update_attributes(intnotes => $intnotes); + $self->js ->val('#order_intnotes', $intnotes) ->run('kivi.Order.close_email_dialog') @@ -418,7 +491,7 @@ sub action_show_periodic_invoices_config_dialog { $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id}; $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm', order_value_periodicity => 'p', # = same as periodicity - start_date_as_date => $::form->{transdate} || $::form->current_date, + start_date_as_date => $::form->{transdate_as_date} || $::form->current_date, extend_automatically_by => 12, active => 1, email_subject => GenericTranslations->get( @@ -439,6 +512,7 @@ sub action_show_periodic_invoices_config_dialog { if ($::form->{customer_id}) { $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]); + $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail; } $self->render('oe/edit_periodic_invoices_config', { layout => 0 }, @@ -477,7 +551,7 @@ sub action_assign_periodic_invoices_config { email_body => $::form->{email_body}, }; - my $periodic_invoices_config = YAML::Dump($config); + my $periodic_invoices_config = SL::YAML::Dump($config); my $status = $self->get_periodic_invoices_status($config); @@ -606,6 +680,7 @@ sub action_customer_vendor_changed { ->val( '#order_payment_id', $self->order->payment_id) ->val( '#order_delivery_term_id', $self->order->delivery_term_id) ->val( '#order_intnotes', $self->order->intnotes) + ->val( '#language_id', $self->order->$cv_method->language_id) ->focus( '#order_' . $self->cv . '_id'); $self->js_redisplay_amounts_and_taxes; @@ -834,9 +909,17 @@ sub action_reorder_items { my $method = $sort_keys{$::form->{order_by}}; my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted }; if ($::form->{sort_dir}) { - @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort; + if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){ + @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort; + } else { + @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort; + } } else { - @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort; + if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){ + @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort; + } else { + @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort; + } } $self->js ->run('kivi.Order.redisplay_items', \@to_sort) @@ -906,8 +989,8 @@ sub js_load_second_row { my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type); $self->js - ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html) - ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1); + ->html('#second_row_' . $item_id, $row_as_html) + ->data('#second_row_' . $item_id, 'loaded', 1); } sub js_redisplay_line_values { @@ -950,6 +1033,19 @@ sub js_redisplay_amounts_and_taxes { $self->js->show('#subtotal_row_id'); } + if ($self->order->is_sales) { + my $is_neg = $self->order->marge_total < 0; + $self->js + ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2)) + ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2)) + ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0') + ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0') + ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0') + ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0') + ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0') + ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0'); + } + $self->js ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2)) ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2)) @@ -957,6 +1053,27 @@ sub js_redisplay_amounts_and_taxes { ->insertBefore($self->build_tax_rows, '#amount_row_id'); } +sub js_reset_order_and_item_ids_after_save { + my ($self) = @_; + + $self->js + ->val('#id', $self->order->id) + ->val('#converted_from_oe_id', '') + ->val('#order_' . $self->nr_key(), $self->order->number); + + my $idx = 0; + foreach my $form_item_id (@{ $::form->{orderitem_ids} }) { + next if !$self->order->items_sorted->[$idx]->id; + next if $form_item_id !~ m{^new}; + $self->js + ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id) + ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id) + ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id); + $idx++; + } + $self->js->val('[name="converted_from_orderitems_ids[+]"]', ''); +} + # # helpers # @@ -1120,7 +1237,7 @@ sub make_order { # be retrieved via items until the order is saved. Adding empty items to new # order here solves this problem. my $order; - $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id}; + $order = SL::DB::Order->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id}; $order ||= SL::DB::Order->new(orderitems => [], quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))); @@ -1135,8 +1252,10 @@ sub make_order { $order->assign_attributes(%{$::form->{order}}); - my $periodic_invoices_config = make_periodic_invoices_config_from_yaml($form_periodic_invoices_config); - $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config; + if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? SL::YAML::Load($form_periodic_invoices_config) : undef) { + my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new); + $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs); + } # remove deleted items $self->item_ids_to_delete([]); @@ -1265,7 +1384,7 @@ sub setup_order_from_cv { $order->intnotes($order->customervendor->notes); if ($order->is_sales) { - $order->salesman_id($order->customer->salesman_id); + $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id); $order->taxincluded(defined($order->customer->taxincluded_checked) ? $order->customer->taxincluded_checked : $::myconfig{taxincluded_checked}); @@ -1293,7 +1412,7 @@ sub recalc { tax => $tax }); } - pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}}; + pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items_sorted}, @{$pat{items}}; } # get data for saving, printing, ..., that is not changed in the form @@ -1344,13 +1463,17 @@ sub save { my $db = $self->order->db; $db->with_transaction(sub { - SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete}; + SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []}; $self->order->save(cascade => 1); # link records if ($::form->{converted_from_oe_id}) { - SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order); - + my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id}; + foreach my $converted_from_oe_id (@converted_from_oe_ids) { + my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load; + $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/; + $src->link_to_record($self->order); + } if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) { my $idx = 0; foreach (@{ $self->order->items_sorted }) { @@ -1374,6 +1497,14 @@ sub save { sub workflow_sales_or_purchase_order { my ($self) = @_; + # always save + my $errors = $self->save(); + + if (scalar @{ $errors }) { + $self->js->flash('error', $_) foreach @{ $errors }; + return $self->js->render(); + } + my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type() : $::form->{type} eq request_quotation_type() ? purchase_order_type() : $::form->{type} eq purchase_order_type() ? sales_order_type() @@ -1440,7 +1571,7 @@ sub pre_render { show_headers => 1, no_queue => 1, no_postscript => 1, - no_opendocument => 1, + no_opendocument => 0, no_html => 1}, ); @@ -1450,6 +1581,11 @@ sub pre_render { $item->active_discount_source($price_source->discount_from_source($item->active_discount_source)); } + if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) { + # calculate shipped qtys here to prevent calling calculate for every item via the items method + SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects; + } + if ($self->order->number && $::instance_conf->get_webdav) { my $webdav = SL::Webdav->new( type => $self->type, @@ -1489,19 +1625,6 @@ sub setup_edit_action_bar { checks => [ 'kivi.Order.check_save_active_periodic_invoices' ], disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef, ], - action => [ - t8('Save and Delivery Order'), - call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts, - $::instance_conf->get_order_warn_no_deliverydate, - ], - checks => [ 'kivi.Order.check_save_active_periodic_invoices' ], - only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) - ], - action => [ - t8('Save and Invoice'), - call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ], - checks => [ 'kivi.Order.check_save_active_periodic_invoices' ], - ], ], # end of combobox "Save" combobox => [ @@ -1509,17 +1632,30 @@ sub setup_edit_action_bar { t8('Workflow'), ], action => [ - t8('Sales Order'), + t8('Save and Sales Order'), submit => [ '#order_form', { action => "Order/sales_order" } ], only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())), disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef, ], action => [ - t8('Purchase Order'), + t8('Save and Purchase Order'), submit => [ '#order_form', { action => "Order/purchase_order" } ], only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())), disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef, ], + action => [ + t8('Save and Delivery Order'), + call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts, + $::instance_conf->get_order_warn_no_deliverydate, + ], + checks => [ 'kivi.Order.check_save_active_periodic_invoices' ], + only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) + ], + action => [ + t8('Save and Invoice'), + call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ], + checks => [ 'kivi.Order.check_save_active_periodic_invoices' ], + ], ], # end of combobox "Workflow" combobox => [ @@ -1527,12 +1663,12 @@ sub setup_edit_action_bar { t8('Export'), ], action => [ - t8('Print'), - call => [ 'kivi.Order.show_print_options' ], + t8('Save and print'), + call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ], ], action => [ - t8('E-mail'), - call => [ 'kivi.Order.email' ], + t8('Save and E-mail'), + call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ], ], action => [ t8('Download attachments of all parts'), @@ -1569,9 +1705,17 @@ sub generate_pdf { $order->language($params->{language}); $order->flatten_to_form($print_form, format_amounts => 1); + my $template_ext; + my $template_type; + if ($print_form->{format} =~ /(opendocument|oasis)/i) { + $template_ext = 'odt'; + $template_type = 'OpenDocument'; + } + # search for the template my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template( name => $print_form->{formname}, + extension => $template_ext, email => $print_form->{media} eq 'email', language => $params->{language}, printer_id => $print_form->{printer_id}, # todo @@ -1588,8 +1732,10 @@ sub generate_pdf { $print_form->prepare_for_printing; $$pdf_ref = SL::Helper::CreatePDF->create_pdf( - template => $template_file, - variables => $print_form, + format => $print_form->{format}, + template_type => $template_type, + template => $template_file, + variables => $print_form, variable_content_types => { longdescription => 'html', partnotes => 'html', @@ -1597,7 +1743,7 @@ sub generate_pdf { }, ); 1; - } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR; + } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR; }); return @errors; @@ -1639,7 +1785,7 @@ sub make_periodic_invoices_config_from_yaml { my ($yaml_config) = @_; return if !$yaml_config; - my $attr = YAML::Load($yaml_config); + my $attr = SL::YAML::Load($yaml_config); return if 'HASH' ne ref $attr; return SL::DB::PeriodicInvoicesConfig->new(%$attr); } @@ -1742,11 +1888,6 @@ Possibility to enter more than one item at once. =item * -Save order only on "save" (and "save and delivery order"-workflow). No -hidden save on "print" or "email". - -=item * - Item list in a scrollable area, so that the workflow buttons stay at the bottom. @@ -1836,7 +1977,7 @@ java script functions =item * credit limit -=item * more workflows (save as new, quotation, purchase order) +=item * more workflows (quotation, rfq) =item * price sources: little symbols showing better price / better discount @@ -1941,12 +2082,6 @@ editor or on text processing application). A warning when leaving the page without saveing unchanged inputs. -=item * - -Workflows for delivery order and invoice are in the menu "Save", because the -order is saved before opening the new document form. Nevertheless perhaps these -workflow buttons should be put under "Workflows". - =back