1 package SL::Controller::DeliveryOrder;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Helper::Number qw(_format_number_units _parse_number);
8 use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
9 use SL::Locale::String qw(t8);
10 use SL::SessionFile::Random;
15 use SL::Util qw(trim);
22 use SL::DB::PartClassification;
23 use SL::DB::PartsGroup;
26 use SL::DB::RecordLink;
28 use SL::DB::Translation;
29 use SL::DB::TransferType;
31 use SL::Helper::CreatePDF qw(:all);
32 use SL::Helper::PrintOptions;
33 use SL::Helper::ShippedQty;
34 use SL::Helper::UserPreferences::PositionsScrollbar;
35 use SL::Helper::UserPreferences::UpdatePositions;
37 use SL::Controller::Helper::GetModels;
38 use SL::Controller::DeliveryOrder::TypeData;
40 use List::Util qw(first sum0);
41 use List::UtilsBy qw(sort_by uniq_by);
42 use List::MoreUtils qw(any none pairwise first_index);
43 use English qw(-no_match_vars);
48 use Rose::Object::MakeMethods::Generic
50 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
51 'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors search_cvpartnumber show_update_button part_picker_classification_ids type_data) ],
56 __PACKAGE__->run_before('check_auth',
57 except => [ qw(pack_stock_information) ]);
59 __PACKAGE__->run_before('get_unalterable_data',
60 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
71 $self->order->transdate(DateTime->now_local());
72 $self->type_data->set_reqdate_by_type;
77 'delivery_order/form',
78 title => $self->get_title_for('add'),
79 %{$self->{template_args}}
83 sub action_add_from_order {
85 # this interfers with init_order
86 $self->{converted_from_oe_id} = delete $::form->{id};
88 # TODO copy data and remember to link them on save
93 # edit an existing order
101 # this is to edit an order from an unsaved order object
103 # set item ids to new fake id, to identify them as new items
104 foreach my $item (@{$self->order->items_sorted}) {
105 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
107 # trigger rendering values for second row as hidden, because they
108 # are loaded only on demand. So we need to keep the values from
110 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
115 'delivery_order/form',
116 title => $self->get_title_for('edit'),
117 %{$self->{template_args}}
121 # edit a collective order (consisting of one or more existing orders)
122 sub action_edit_collective {
126 my @multi_ids = map {
127 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
128 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
130 # fall back to add if no ids are given
131 if (scalar @multi_ids == 0) {
136 # fall back to save as new if only one id is given
137 if (scalar @multi_ids == 1) {
138 $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
139 $self->action_save_as_new();
143 # make new order from given orders
144 my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
145 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
146 $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
148 $self->action_edit();
155 my $errors = $self->delete();
157 if (scalar @{ $errors }) {
158 $self->js->flash('error', $_) foreach @{ $errors };
159 return $self->js->render();
162 flash_later('info', $self->type_data->text("delete"));
164 my @redirect_params = (
169 $self->redirect_to(@redirect_params);
176 my $errors = $self->save();
178 if (scalar @{ $errors }) {
179 $self->js->flash('error', $_) foreach @{ $errors };
180 return $self->js->render();
183 flash_later('info', $self->type_data->text("saved"));
185 my @redirect_params = (
188 id => $self->order->id,
191 $self->redirect_to(@redirect_params);
194 # save the order as new document an open it for edit
195 sub action_save_as_new {
198 my $order = $self->order;
201 $self->js->flash('error', t8('This object has not been saved yet.'));
202 return $self->js->render();
205 # load order from db to check if values changed
206 my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
209 # Lets assign a new number if the user hasn't changed the previous one.
210 # If it has been changed manually then use it as-is.
211 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
213 : trim($order->number);
215 # Clear transdate unless changed
216 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
217 ? DateTime->today_local
220 # Set new reqdate unless changed if it is enabled in client config
221 $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
224 $new_attrs{employee} = SL::DB::Manager::Employee->current;
226 # Create new record from current one
227 $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
229 # no linked records on save as new
230 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
233 $self->action_save();
238 # This is called if "print" is pressed in the print dialog.
239 # If PDF creation was requested and succeeded, the pdf is offered for download
240 # via send_file (which uses ajax in this case).
244 my $errors = $self->save();
246 if (scalar @{ $errors }) {
247 $self->js->flash('error', $_) foreach @{ $errors };
248 return $self->js->render();
251 $self->js_reset_order_and_item_ids_after_save;
253 my $format = $::form->{print_options}->{format};
254 my $media = $::form->{print_options}->{media};
255 my $formname = $::form->{print_options}->{formname};
256 my $copies = $::form->{print_options}->{copies};
257 my $groupitems = $::form->{print_options}->{groupitems};
258 my $printer_id = $::form->{print_options}->{printer_id};
260 # only pdf and opendocument by now
261 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
262 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
265 # only screen or printer by now
266 if (none { $media eq $_ } qw(screen printer)) {
267 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
270 # create a form for generate_attachment_filename
271 my $form = Form->new;
272 $form->{$self->nr_key()} = $self->order->number;
273 $form->{type} = $self->type;
274 $form->{format} = $format;
275 $form->{formname} = $formname;
276 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
277 my $pdf_filename = $form->generate_attachment_filename();
280 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
281 formname => $formname,
282 language => $self->order->language,
283 printer_id => $printer_id,
284 groupitems => $groupitems });
285 if (scalar @errors) {
286 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
289 if ($media eq 'screen') {
291 $self->js->flash('info', t8('The PDF has been created'));
294 type => SL::MIME->mime_type_from_ext($pdf_filename),
295 name => $pdf_filename,
299 } elsif ($media eq 'printer') {
301 my $printer_id = $::form->{print_options}->{printer_id};
302 SL::DB::Printer->new(id => $printer_id)->load->print_document(
307 $self->js->flash('info', t8('The PDF has been printed'));
310 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
311 if (scalar @warnings) {
312 $self->js->flash('warning', $_) for @warnings;
315 $self->save_history('PRINTED');
318 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
321 sub action_preview_pdf {
324 my $errors = $self->save();
325 if (scalar @{ $errors }) {
326 $self->js->flash('error', $_) foreach @{ $errors };
327 return $self->js->render();
330 $self->js_reset_order_and_item_ids_after_save;
333 my $media = 'screen';
334 my $formname = $self->type;
337 # create a form for generate_attachment_filename
338 my $form = Form->new;
339 $form->{$self->nr_key()} = $self->order->number;
340 $form->{type} = $self->type;
341 $form->{format} = $format;
342 $form->{formname} = $formname;
343 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
344 my $pdf_filename = $form->generate_attachment_filename();
347 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
348 formname => $formname,
349 language => $self->order->language,
351 if (scalar @errors) {
352 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
354 $self->save_history('PREVIEWED');
355 $self->js->flash('info', t8('The PDF has been previewed'));
359 type => SL::MIME->mime_type_from_ext($pdf_filename),
360 name => $pdf_filename,
365 # open the email dialog
366 sub action_save_and_show_email_dialog {
369 my $errors = $self->save();
371 if (scalar @{ $errors }) {
372 $self->js->flash('error', $_) foreach @{ $errors };
373 return $self->js->render();
376 my $cv_method = $self->cv;
378 if (!$self->order->$cv_method) {
379 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'))
384 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
385 $email_form->{to} ||= $self->order->$cv_method->email;
386 $email_form->{cc} = $self->order->$cv_method->cc;
387 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
388 # Todo: get addresses from shipto, if any
390 my $form = Form->new;
391 $form->{$self->nr_key()} = $self->order->number;
392 $form->{cusordnumber} = $self->order->cusordnumber;
393 $form->{formname} = $self->type;
394 $form->{type} = $self->type;
395 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
396 $form->{language_id} = $self->order->language->id if $self->order->language;
397 $form->{format} = 'pdf';
398 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
400 $email_form->{subject} = $form->generate_email_subject();
401 $email_form->{attachment_filename} = $form->generate_attachment_filename();
402 $email_form->{message} = $form->generate_email_body();
403 $email_form->{js_send_function} = 'kivi.Order.send_email()';
405 my %files = $self->get_files_for_email_dialog();
406 $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
407 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
408 email_form => $email_form,
409 show_bcc => $::auth->assert('email_bcc', 'may fail'),
411 is_customer => $self->type_data->is_customer,
412 ALL_EMPLOYEES => $self->{all_employees},
416 ->run('kivi.Order.show_email_dialog', $dialog_html)
423 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
424 sub action_send_email {
427 my $errors = $self->save();
429 if (scalar @{ $errors }) {
430 $self->js->run('kivi.Order.close_email_dialog');
431 $self->js->flash('error', $_) foreach @{ $errors };
432 return $self->js->render();
435 $self->js_reset_order_and_item_ids_after_save;
437 my $email_form = delete $::form->{email_form};
438 my %field_names = (to => 'email');
440 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
442 # for Form::cleanup which may be called in Form::send_email
443 $::form->{cwd} = getcwd();
444 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
446 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
447 $::form->{media} = 'email';
449 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
451 my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
452 format => $::form->{print_options}->{format},
453 formname => $::form->{print_options}->{formname},
454 language => $self->order->language,
455 printer_id => $::form->{print_options}->{printer_id},
456 groupitems => $::form->{print_options}->{groupitems}});
457 if (scalar @errors) {
458 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
461 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
462 if (scalar @warnings) {
463 flash_later('warning', $_) for @warnings;
466 my $sfile = SL::SessionFile::Random->new(mode => "w");
467 $sfile->fh->print($pdf);
470 $::form->{tmpfile} = $sfile->file_name;
471 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
474 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
475 $::form->send_email(\%::myconfig, 'pdf');
478 my $intnotes = $self->order->intnotes;
479 $intnotes .= "\n\n" if $self->order->intnotes;
480 $intnotes .= t8('[email]') . "\n";
481 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
482 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
483 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
484 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
485 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
486 $intnotes .= t8('Message') . ": " . $::form->{message};
488 $self->order->update_attributes(intnotes => $intnotes);
490 $self->save_history('MAILED');
492 flash_later('info', t8('The email has been sent.'));
494 my @redirect_params = (
497 id => $self->order->id,
500 $self->redirect_to(@redirect_params);
503 # save the order and redirect to the frontend subroutine for a new
505 sub action_save_and_delivery_order {
508 $self->save_and_redirect_to(
509 controller => 'oe.pl',
510 action => 'oe_delivery_order_from_order',
514 # save the order and redirect to the frontend subroutine for a new
516 sub action_save_and_invoice {
519 $self->save_and_redirect_to(
520 controller => 'oe.pl',
521 action => 'oe_invoice_from_order',
525 # workflow from sales order to sales quotation
526 sub action_sales_quotation {
527 $_[0]->workflow_sales_or_request_for_quotation();
530 # workflow from sales order to sales quotation
531 sub action_request_for_quotation {
532 $_[0]->workflow_sales_or_request_for_quotation();
535 # workflow from sales quotation to sales order
536 sub action_sales_order {
537 $_[0]->workflow_sales_or_purchase_order();
540 # workflow from rfq to purchase order
541 sub action_purchase_order {
542 $_[0]->workflow_sales_or_purchase_order();
545 # workflow from purchase order to ap transaction
546 sub action_save_and_ap_transaction {
549 $self->save_and_redirect_to(
550 controller => 'ap.pl',
551 action => 'add_from_purchase_order',
555 # set form elements in respect to a changed customer or vendor
557 # This action is called on an change of the customer/vendor picker.
558 sub action_customer_vendor_changed {
561 setup_order_from_cv($self->order);
563 my $cv_method = $self->cv;
565 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
566 $self->js->show('#cp_row');
568 $self->js->hide('#cp_row');
571 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
572 $self->js->show('#shipto_selection');
574 $self->js->hide('#shipto_selection');
577 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
580 ->replaceWith('#order_cp_id', $self->build_contact_select)
581 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
582 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
583 ->replaceWith('#business_info_row', $self->build_business_info_row)
584 ->val( '#order_taxzone_id', $self->order->taxzone_id)
585 ->val( '#order_taxincluded', $self->order->taxincluded)
586 ->val( '#order_currency_id', $self->order->currency_id)
587 ->val( '#order_payment_id', $self->order->payment_id)
588 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
589 ->val( '#order_intnotes', $self->order->intnotes)
590 ->val( '#order_language_id', $self->order->$cv_method->language_id)
591 ->focus( '#order_' . $self->cv . '_id')
592 ->run('kivi.Order.update_exchangerate');
594 $self->js_redisplay_cvpartnumbers;
598 # open the dialog for customer/vendor details
599 sub action_show_customer_vendor_details_dialog {
602 my $is_customer = 'customer' eq $::form->{vc};
605 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
607 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
610 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
611 $details{discount_as_percent} = $cv->discount_as_percent;
612 $details{creditlimt} = $cv->creditlimit_as_number;
613 $details{business} = $cv->business->description if $cv->business;
614 $details{language} = $cv->language_obj->description if $cv->language_obj;
615 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
616 $details{payment_terms} = $cv->payment->description if $cv->payment;
617 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
619 foreach my $entry (@{ $cv->shipto }) {
620 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
622 foreach my $entry (@{ $cv->contacts }) {
623 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
626 $_[0]->render('common/show_vc_details', { layout => 0 },
627 is_customer => $is_customer,
632 # called if a unit in an existing item row is changed
633 sub action_unit_changed {
636 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
637 my $item = $self->order->items_sorted->[$idx];
639 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
640 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
643 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
644 $self->js_redisplay_line_values;
648 # add an item row for a new item entered in the input row
649 sub action_add_item {
652 delete $::form->{add_item}->{create_part_type};
654 my $form_attr = $::form->{add_item};
656 return unless $form_attr->{parts_id};
658 my $item = new_item($self->order, $form_attr);
660 $self->order->add_items($item);
662 $self->get_item_cvpartnumber($item);
664 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
665 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
669 in_out => $self->type_data->transfer,
672 if ($::form->{insert_before_item_id}) {
674 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
677 ->append('#row_table_id', $row_as_html);
680 if ( $item->part->is_assortment ) {
681 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
682 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
683 my $attr = { parts_id => $assortment_item->parts_id,
684 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
685 unit => $assortment_item->unit,
686 description => $assortment_item->part->description,
688 my $item = new_item($self->order, $attr);
690 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
691 $item->discount(1) unless $assortment_item->charge;
693 $self->order->add_items( $item );
694 $self->get_item_cvpartnumber($item);
695 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
696 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
701 if ($::form->{insert_before_item_id}) {
703 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
706 ->append('#row_table_id', $row_as_html);
712 ->val('.add_item_input', '')
713 ->run('kivi.Order.init_row_handlers')
714 ->run('kivi.Order.renumber_positions')
715 ->focus('#add_item_parts_id_name');
717 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
722 # add item rows for multiple items at once
723 sub action_add_multi_items {
726 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
727 return $self->js->render() unless scalar @form_attr;
730 foreach my $attr (@form_attr) {
731 my $item = new_item($self->order, $attr);
733 if ( $item->part->is_assortment ) {
734 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
735 my $attr = { parts_id => $assortment_item->parts_id,
736 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
737 unit => $assortment_item->unit,
738 description => $assortment_item->part->description,
740 my $item = new_item($self->order, $attr);
742 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
743 $item->discount(1) unless $assortment_item->charge;
748 $self->order->add_items(@items);
750 foreach my $item (@items) {
751 $self->get_item_cvpartnumber($item);
752 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
753 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
757 in_out => $self->type_data->transfer,
760 if ($::form->{insert_before_item_id}) {
762 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
765 ->append('#row_table_id', $row_as_html);
770 ->run('kivi.Part.close_picker_dialogs')
771 ->run('kivi.Order.init_row_handlers')
772 ->run('kivi.Order.renumber_positions')
773 ->focus('#add_item_parts_id_name');
775 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
780 sub action_update_exchangerate {
784 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
785 currency_name => $self->order->currency->name,
788 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
791 # redisplay item rows if they are sorted by an attribute
792 sub action_reorder_items {
796 partnumber => sub { $_[0]->part->partnumber },
797 description => sub { $_[0]->description },
798 qty => sub { $_[0]->qty },
799 sellprice => sub { $_[0]->sellprice },
800 discount => sub { $_[0]->discount },
801 cvpartnumber => sub { $_[0]->{cvpartnumber} },
804 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
806 my $method = $sort_keys{$::form->{order_by}};
807 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
808 if ($::form->{sort_dir}) {
809 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
810 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
812 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
815 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
816 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
818 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
822 ->run('kivi.Order.redisplay_items', \@to_sort)
826 # show the popup to choose a price/discount source
827 sub action_price_popup {
830 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
831 my $item = $self->order->items_sorted->[$idx];
833 $self->render_price_dialog($item);
836 # save the order in a session variable and redirect to the part controller
837 sub action_create_part {
840 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
842 my $callback = $self->url_for(
843 action => 'return_from_create_part',
844 type => $self->type, # type is needed for check_auth on return
845 previousform => $previousform,
848 flash_later('info', t8('You are adding a new part while you are editing another document. You will be redirected to your document when saving the new part or aborting this form.'));
850 my @redirect_params = (
851 controller => 'Part',
853 part_type => $::form->{add_item}->{create_part_type},
854 callback => $callback,
858 $self->redirect_to(@redirect_params);
861 sub action_return_from_create_part {
864 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
866 $::auth->restore_form_from_session(delete $::form->{previousform});
868 # set item ids to new fake id, to identify them as new items
869 foreach my $item (@{$self->order->items_sorted}) {
870 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
873 $self->get_unalterable_data();
876 # trigger rendering values for second row/longdescription as hidden,
877 # because they are loaded only on demand. So we need to keep the values
879 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
880 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
883 'delivery_order/form',
884 title => $self->get_title_for('edit'),
885 %{$self->{template_args}}
890 sub action_stock_in_out_dialog {
893 my $part = SL::DB::Part->load_cached($::form->{parts_id}) or die "need parts_id";
894 my $stock = $::form->{stock};
895 my $unit = $::form->{unit};
896 my $qty = _parse_number($::form->{qty_as_number});
898 my $inout = $self->type_data->transfer;
900 my @contents = DO->get_item_availability(parts_id => $part->id);
901 my $stock_info = DO->unpack_stock_information(packed => $stock);
903 $self->merge_stock_data($stock_info, \@contents, $part);
905 $self->render("delivery_order/stock_dialog", { layout => 0 },
906 WHCONTENTS => $self->order->delivered ? $stock_info : \@contents,
910 delivered => $self->order->delivered,
914 # we're using the old YAML based stock packing, but don't want to do this in
915 # the frontend so we're doing a tiny roundtrip to the backend, back the info in
916 # perl, serve it back to the frontend and store it in the DOM there
917 sub action_pack_stock_information {
920 my $stock_info = $::form->{stock_info};
921 my $yaml = SL::YAML::Dump($stock_info);
923 $self->render(\$yaml, { layout => 0, process => 0 });
926 sub merge_stock_data {
927 my ($self, $stock_info, $contents, $part) = @_;
928 # TODO rewrite to mapping
930 if (!$self->order->delivered) {
931 for my $row (@$contents) {
932 $row->{available_qty} = _format_number_units($row->{qty}, $row->{unit}, $part->unit);
934 for my $sinfo (@{ $stock_info }) {
935 next if $row->{bin_id} != $sinfo->{bin_id} ||
936 $row->{warehouse_id} != $sinfo->{warehouse_id} ||
937 $row->{chargenumber} ne $sinfo->{chargenumber} ||
938 $row->{bestbefore} ne $sinfo->{bestbefore};
940 $row->{"stock_$_"} = $sinfo->{$_}
941 for qw(qty unit error delivery_order_items_stock_id);
946 for my $sinfo (@{ $stock_info }) {
947 my $bin = SL::DB::Bin->load_cached($sinfo->{bin_id});
948 $sinfo->{warehouse_description} = $bin->warehouse->description;
949 $sinfo->{bin_description} = $bin->escription;
950 map { $sinfo->{"stock_$_"} = $sinfo->{$_} } qw(qty unit);
955 # load the second row for one or more items
957 # This action gets the html code for all items second rows by rendering a template for
958 # the second row and sets the html code via client js.
959 sub action_load_second_rows {
962 foreach my $item_id (@{ $::form->{item_ids} }) {
963 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
964 my $item = $self->order->items_sorted->[$idx];
966 $self->js_load_second_row($item, $item_id, 0);
969 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
974 # update description, notes and sellprice from master data
975 sub action_update_row_from_master_data {
978 foreach my $item_id (@{ $::form->{item_ids} }) {
979 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
980 my $item = $self->order->items_sorted->[$idx];
981 my $texts = get_part_texts($item->part, $self->order->language_id);
983 $item->description($texts->{description});
984 $item->longdescription($texts->{longdescription});
986 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
989 if ($item->part->is_assortment) {
990 # add assortment items with price 0, as the components carry the price
991 $price_src = $price_source->price_from_source("");
992 $price_src->price(0);
994 $price_src = $price_source->best_price
995 ? $price_source->best_price
996 : $price_source->price_from_source("");
997 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
998 $price_src->price(0) if !$price_source->best_price;
1002 $item->sellprice($price_src->price);
1003 $item->active_price_source($price_src);
1006 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
1007 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
1008 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
1009 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
1011 if ($self->search_cvpartnumber) {
1012 $self->get_item_cvpartnumber($item);
1013 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
1017 $self->js_redisplay_line_values;
1019 $self->js->render();
1022 sub action_transfer_stock {
1025 if ($self->order->delivered) {
1026 return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
1029 my $errors = $self->save;
1032 $self->js->flash('error', $_) for @$errors;
1033 return $self->js->render;
1036 my $order = $self->order;
1038 # TODO move to type data
1039 my $trans_type = $self->type_data->properties('transfer') eq 'in'
1040 ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
1041 : SL::DB::Manager::TransferType->find_by(direction => "out", deescription => "shipped");
1043 my @transfer_requests;
1045 for my $item (@{ $order->items_sorted }) {
1046 for my $stock (@{ $item->delivery_order_stock_entries }) {
1047 my $transfer = SL::DB::Inventory->new_from($stock);
1048 $transfer->trans_type($trans_type);
1050 push @transfer_requests, $transfer;
1054 if (!@transfer_requests) {
1055 $self->js->flash("error", t8("No stock to transfer"))->render;
1058 SL::DB->with_transaction(sub {
1059 $_->save for @transfer_requests;
1060 $self->order->update_attributes(deliverd => 1);
1063 $self->js->flash("info", t8("Stock transfered"))->render;
1066 sub js_load_second_row {
1067 my ($self, $item, $item_id, $do_parse) = @_;
1070 # Parse values from form (they are formated while rendering (template)).
1071 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1072 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1073 foreach my $var (@{ $item->cvars_by_config }) {
1074 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1076 $item->parse_custom_variable_values;
1079 my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1082 ->html('#second_row_' . $item_id, $row_as_html)
1083 ->data('#second_row_' . $item_id, 'loaded', 1);
1086 sub js_redisplay_line_values {
1089 my $is_sales = $self->order->is_sales;
1091 # sales orders with margins
1096 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1097 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1098 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1099 ]} @{ $self->order->items_sorted };
1103 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1104 ]} @{ $self->order->items_sorted };
1108 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1111 sub js_redisplay_cvpartnumbers {
1114 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1116 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1119 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1122 sub js_reset_order_and_item_ids_after_save {
1126 ->val('#id', $self->order->id)
1127 ->val('#converted_from_oe_id', '')
1128 ->val('#order_' . $self->nr_key(), $self->order->number);
1131 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1132 next if !$self->order->items_sorted->[$idx]->id;
1133 next if $form_item_id !~ m{^new};
1135 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1136 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1137 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1141 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1151 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1152 die "Not a valid type for delivery order";
1155 $self->type($::form->{type});
1161 return $self->type_data->customervendor;
1164 sub init_search_cvpartnumber {
1167 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1168 my $search_cvpartnumber;
1169 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1170 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1172 return $search_cvpartnumber;
1175 sub init_show_update_button {
1178 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1189 sub init_all_price_factors {
1190 SL::DB::Manager::PriceFactor->get_all;
1193 sub init_part_picker_classification_ids {
1196 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
1202 $::auth->assert($self->type_data->access || 'DOES_NOT_EXIST');
1205 # build the selection box for contacts
1207 # Needed, if customer/vendor changed.
1208 sub build_contact_select {
1211 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1212 value_key => 'cp_id',
1213 title_key => 'full_name_dep',
1214 default => $self->order->cp_id,
1216 style => 'width: 300px',
1220 # build the selection box for shiptos
1222 # Needed, if customer/vendor changed.
1223 sub build_shipto_select {
1226 select_tag('order.shipto_id',
1227 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1228 value_key => 'shipto_id',
1229 title_key => 'displayable_id',
1230 default => $self->order->shipto_id,
1232 style => 'width: 300px',
1236 # build the inputs for the cusom shipto dialog
1238 # Needed, if customer/vendor changed.
1239 sub build_shipto_inputs {
1242 my $content = $self->p->render('common/_ship_to_dialog',
1243 vc_obj => $self->order->customervendor,
1244 cs_obj => $self->order->custom_shipto,
1245 cvars => $self->order->custom_shipto->cvars_by_config,
1246 id_selector => '#order_shipto_id');
1248 div_tag($content, id => 'shipto_inputs');
1251 # render the info line for business
1253 # Needed, if customer/vendor changed.
1254 sub build_business_info_row
1256 $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
1263 return if !$::form->{id};
1265 $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
1267 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1268 # You need a custom shipto object to call cvars_by_config to get the cvars.
1269 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1271 return $self->order;
1274 # load or create a new order object
1276 # And assign changes from the form to this object.
1277 # If the order is loaded from db, check if items are deleted in the form,
1278 # remove them form the object and collect them for removing from db on saving.
1279 # Then create/update items from form (via make_item) and add them.
1283 # add_items adds items to an order with no items for saving, but they cannot
1284 # be retrieved via items until the order is saved. Adding empty items to new
1285 # order here solves this problem.
1287 $order = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1288 $order ||= SL::DB::DeliveryOrder->new(orderitems => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
1290 my $cv_id_method = $self->cv . '_id';
1291 if (!$::form->{id} && $::form->{$cv_id_method}) {
1292 $order->$cv_id_method($::form->{$cv_id_method});
1293 setup_order_from_cv($order);
1296 my $form_orderitems = delete $::form->{order}->{orderitems};
1298 $order->assign_attributes(%{$::form->{order}});
1300 $self->setup_custom_shipto_from_form($order, $::form);
1302 # remove deleted items
1303 $self->item_ids_to_delete([]);
1304 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1305 my $item = $order->orderitems->[$idx];
1306 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1307 splice @{$order->orderitems}, $idx, 1;
1308 push @{$self->item_ids_to_delete}, $item->id;
1314 foreach my $form_attr (@{$form_orderitems}) {
1315 my $item = make_item($order, $form_attr);
1316 $item->position($pos);
1320 $order->add_items(grep {!$_->id} @items);
1325 # create or update items from form
1327 # Make item objects from form values. For items already existing read from db.
1328 # Create a new item else. And assign attributes.
1330 my ($record, $attr) = @_;
1333 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1335 my $is_new = !$item;
1337 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1338 # they cannot be retrieved via custom_variables until the order/orderitem is
1339 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1340 $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
1342 $item->assign_attributes(%$attr);
1345 my $texts = get_part_texts($item->part, $record->language_id);
1346 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1347 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1348 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1356 # This is used to add one item
1358 my ($record, $attr) = @_;
1360 my $item = SL::DB::DeliveryOrderItem->new;
1362 # Remove attributes where the user left or set the inputs empty.
1363 # So these attributes will be undefined and we can distinguish them
1364 # from zero later on.
1365 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1366 delete $attr->{$_} if $attr->{$_} eq '';
1369 $item->assign_attributes(%$attr);
1371 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1372 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1374 $item->unit($part->unit) if !$item->unit;
1377 if ( $part->is_assortment ) {
1378 # add assortment items with price 0, as the components carry the price
1379 $price_src = $price_source->price_from_source("");
1380 $price_src->price(0);
1381 } elsif (defined $item->sellprice) {
1382 $price_src = $price_source->price_from_source("");
1383 $price_src->price($item->sellprice);
1385 $price_src = $price_source->best_price
1386 ? $price_source->best_price
1387 : $price_source->price_from_source("");
1388 $price_src->price(0) if !$price_source->best_price;
1392 if (defined $item->discount) {
1393 $discount_src = $price_source->discount_from_source("");
1394 $discount_src->discount($item->discount);
1396 $discount_src = $price_source->best_discount
1397 ? $price_source->best_discount
1398 : $price_source->discount_from_source("");
1399 $discount_src->discount(0) if !$price_source->best_discount;
1403 $new_attr{part} = $part;
1404 $new_attr{description} = $part->description if ! $item->description;
1405 $new_attr{qty} = 1.0 if ! $item->qty;
1406 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1407 $new_attr{sellprice} = $price_src->price;
1408 $new_attr{discount} = $discount_src->discount;
1409 $new_attr{active_price_source} = $price_src;
1410 $new_attr{active_discount_source} = $discount_src;
1411 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1412 $new_attr{project_id} = $record->globalproject_id;
1413 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1415 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1416 # they cannot be retrieved via custom_variables until the order/orderitem is
1417 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1418 $new_attr{custom_variables} = [];
1420 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1422 $item->assign_attributes(%new_attr, %{ $texts });
1427 sub setup_order_from_cv {
1430 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1432 $order->intnotes($order->customervendor->notes);
1434 if ($order->is_sales) {
1435 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1436 $order->taxincluded(defined($order->customer->taxincluded_checked)
1437 ? $order->customer->taxincluded_checked
1438 : $::myconfig{taxincluded_checked});
1443 # setup custom shipto from form
1445 # The dialog returns form variables starting with 'shipto' and cvars starting
1446 # with 'shiptocvar_'.
1447 # Mark it to be deleted if a shipto from master data is selected
1448 # (i.e. order has a shipto).
1449 # Else, update or create a new custom shipto. If the fields are empty, it
1450 # will not be saved on save.
1451 sub setup_custom_shipto_from_form {
1452 my ($self, $order, $form) = @_;
1454 if ($order->shipto) {
1455 $self->is_custom_shipto_to_delete(1);
1457 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1459 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1460 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1462 $custom_shipto->assign_attributes(%$shipto_attrs);
1463 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1467 # get data for saving, printing, ..., that is not changed in the form
1469 # Only cvars for now.
1470 sub get_unalterable_data {
1473 foreach my $item (@{ $self->order->items }) {
1474 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1475 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1476 foreach my $var (@{ $item->cvars_by_config }) {
1477 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1479 $item->parse_custom_variable_values;
1485 # And remove related files in the spool directory
1490 my $db = $self->order->db;
1492 $db->with_transaction(
1494 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1495 $self->order->delete;
1496 my $spool = $::lx_office_conf{paths}->{spool};
1497 unlink map { "$spool/$_" } @spoolfiles if $spool;
1499 $self->save_history('DELETED');
1502 }) || push(@{$errors}, $db->error);
1509 # And delete items that are deleted in the form.
1514 my $db = $self->order->db;
1516 $db->with_transaction(sub {
1517 # delete custom shipto if it is to be deleted or if it is empty
1518 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1519 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1520 $self->order->custom_shipto(undef);
1523 SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1524 $self->order->save(cascade => 1);
1527 if ($::form->{converted_from_oe_id}) {
1528 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1529 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1530 my $src = SL::DB::DeliveryOrder->new(id => $converted_from_oe_id)->load;
1531 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1532 $src->link_to_record($self->order);
1534 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1536 foreach (@{ $self->order->items_sorted }) {
1537 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1539 SL::DB::RecordLink->new(from_table => 'orderitems',
1540 from_id => $from_id,
1541 to_table => 'orderitems',
1549 $self->save_history('SAVED');
1552 }) || push(@{$errors}, $db->error);
1557 sub workflow_sales_or_request_for_quotation {
1561 my $errors = $self->save();
1563 if (scalar @{ $errors }) {
1564 $self->js->flash('error', $_) for @{ $errors };
1565 return $self->js->render();
1568 my $destination_type = $self->type_data->workflow("to_quotation_type");
1570 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1571 $self->{converted_from_oe_id} = delete $::form->{id};
1573 # set item ids to new fake id, to identify them as new items
1574 foreach my $item (@{$self->order->items_sorted}) {
1575 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1579 $::form->{type} = $destination_type;
1580 $self->type($self->init_type);
1581 $self->cv ($self->init_cv);
1584 $self->get_unalterable_data();
1585 $self->pre_render();
1587 # trigger rendering values for second row as hidden, because they
1588 # are loaded only on demand. So we need to keep the values from the
1590 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1593 'delivery_order/form',
1594 title => $self->get_title_for('edit'),
1595 %{$self->{template_args}}
1599 sub workflow_sales_or_purchase_order {
1603 my $errors = $self->save();
1605 if (scalar @{ $errors }) {
1606 $self->js->flash('error', $_) foreach @{ $errors };
1607 return $self->js->render();
1610 my $destination_type = $self->type_data->workflow("to_order_type");
1612 # check for direct delivery
1613 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1615 if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
1616 $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
1619 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1620 $self->{converted_from_oe_id} = delete $::form->{id};
1622 # set item ids to new fake id, to identify them as new items
1623 foreach my $item (@{$self->order->items_sorted}) {
1624 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1627 if ($self->type_data->workflow("to_order_copy_shipto")) {
1628 if ($::form->{use_shipto}) {
1629 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1631 # remove any custom shipto if not wanted
1632 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1637 $::form->{type} = $destination_type;
1638 $self->type($self->init_type);
1639 $self->cv ($self->init_cv);
1642 $self->get_unalterable_data();
1643 $self->pre_render();
1645 # trigger rendering values for second row as hidden, because they
1646 # are loaded only on demand. So we need to keep the values from the
1648 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1651 'delivery_order/form',
1652 title => $self->get_title_for('edit'),
1653 %{$self->{template_args}}
1660 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1661 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1662 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1663 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
1664 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1667 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1670 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1672 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1673 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1674 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1675 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1677 my $print_form = Form->new('');
1678 $print_form->{type} = $self->type;
1679 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1680 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1681 form => $print_form,
1682 options => {dialog_name_prefix => 'print_options.',
1686 no_opendocument => 0,
1690 foreach my $item (@{$self->order->orderitems}) {
1691 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1692 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1693 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1696 if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
1697 my $webdav = SL::Webdav->new(
1698 type => $self->type,
1699 number => $self->order->number,
1701 my @all_objects = $webdav->get_all_objects;
1702 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1704 link => File::Spec->catfile($_->full_filedescriptor),
1708 $self->{template_args}{in_out} = $self->type_data->transfer;
1710 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1712 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
1713 calculate_qty kivi.Validator follow_up show_history);
1714 $self->setup_edit_action_bar;
1717 sub setup_edit_action_bar {
1718 my ($self, %params) = @_;
1720 my $deletion_allowed = $self->type_data->show_menu("delete");
1722 for my $bar ($::request->layout->get('actionbar')) {
1727 call => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1728 $::instance_conf->get_order_warn_no_deliverydate,
1733 call => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1734 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1736 ], # end of combobox "Save"
1743 t8('Save and Quotation'),
1744 submit => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
1745 only_if => $self->type_data->show_menu("save_and_quotation"),
1749 submit => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
1750 only_if => $self->type_data->show_menu("save_and_rfq"),
1753 t8('Save and Sales Order'),
1754 submit => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
1755 only_if => $self->type_data->show_menu("save_and_sales_order"),
1758 t8('Save and Purchase Order'),
1759 call => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
1760 only_if => $self->type_data->show_menu("save_and_purchase_order"),
1763 t8('Save and Delivery Order'),
1764 call => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1765 $::instance_conf->get_order_warn_no_deliverydate,
1767 only_if => $self->type_data->show_menu("save_and_delivery_order"),
1770 t8('Save and Invoice'),
1771 call => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1772 only_if => $self->type_data->show_menu("save_and_invoice"),
1775 t8('Save and AP Transaction'),
1776 call => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
1777 only_if => $self->type_data->show_menu("save_and_ap_transaction"),
1780 ], # end of combobox "Workflow"
1787 t8('Save and preview PDF'),
1788 call => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
1789 $::instance_conf->get_order_warn_no_deliverydate,
1793 t8('Save and print'),
1794 call => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
1795 $::instance_conf->get_order_warn_no_deliverydate,
1799 t8('Save and E-mail'),
1800 id => 'save_and_email_action',
1801 call => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
1802 $::instance_conf->get_order_warn_no_deliverydate,
1804 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1807 t8('Download attachments of all parts'),
1808 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1809 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1810 only_if => $::instance_conf->get_doc_storage,
1812 ], # end of combobox "Export"
1816 call => [ 'kivi.DeliveryOrder.delete_order' ],
1817 confirm => $::locale->text('Do you really want to delete this object?'),
1818 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1819 only_if => $self->type_data->show_menu("delete"),
1825 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1826 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1827 only_if => $self->type_data->properties('transfer') eq 'out',
1828 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1832 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1833 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1834 only_if => $self->type_data->properties('transfer') eq 'in',
1835 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1845 call => [ 'kivi.DeliveryOrder.follow_up_window' ],
1846 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1847 only_if => $::auth->assert('productivity', 1),
1851 call => [ 'set_history_window', $self->order->id, 'id' ],
1852 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
1854 ], # end of combobox "more"
1860 my ($order, $pdf_ref, $params) = @_;
1864 my $print_form = Form->new('');
1865 $print_form->{type} = $order->type;
1866 $print_form->{formname} = $params->{formname} || $order->type;
1867 $print_form->{format} = $params->{format} || 'pdf';
1868 $print_form->{media} = $params->{media} || 'file';
1869 $print_form->{groupitems} = $params->{groupitems};
1870 $print_form->{printer_id} = $params->{printer_id};
1871 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1873 $order->language($params->{language});
1874 $order->flatten_to_form($print_form, format_amounts => 1);
1878 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
1879 $template_ext = 'odt';
1880 $template_type = 'OpenDocument';
1883 # search for the template
1884 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1885 name => $print_form->{formname},
1886 extension => $template_ext,
1887 email => $print_form->{media} eq 'email',
1888 language => $params->{language},
1889 printer_id => $print_form->{printer_id},
1892 if (!defined $template_file) {
1893 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);
1896 return @errors if scalar @errors;
1898 $print_form->throw_on_error(sub {
1900 $print_form->prepare_for_printing;
1902 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1903 format => $print_form->{format},
1904 template_type => $template_type,
1905 template => $template_file,
1906 variables => $print_form,
1907 variable_content_types => {
1908 longdescription => 'html',
1909 partnotes => 'html',
1914 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
1920 sub get_files_for_email_dialog {
1923 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1925 return %files if !$::instance_conf->get_doc_storage;
1927 if ($self->order->id) {
1928 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1929 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1930 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1931 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
1935 uniq_by { $_->{id} }
1937 +{ id => $_->part->id,
1938 partnumber => $_->part->partnumber }
1939 } @{$self->order->items_sorted};
1941 foreach my $part (@parts) {
1942 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1943 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1946 foreach my $key (keys %files) {
1947 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1954 my ($self, $action) = @_;
1956 return '' if none { lc($action)} qw(add edit);
1957 return $self->type_data->text($action);
1960 sub get_item_cvpartnumber {
1961 my ($self, $item) = @_;
1963 return if !$self->search_cvpartnumber;
1964 return if !$self->order->customervendor;
1966 if ($self->cv eq 'vendor') {
1967 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
1968 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
1969 } elsif ($self->cv eq 'customer') {
1970 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
1971 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
1975 sub get_part_texts {
1976 my ($part_or_id, $language_or_id, %defaults) = @_;
1978 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
1979 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
1981 description => $defaults{description} // $part->description,
1982 longdescription => $defaults{longdescription} // $part->notes,
1985 return $texts unless $language_id;
1987 my $translation = SL::DB::Manager::Translation->get_first(
1989 parts_id => $part->id,
1990 language_id => $language_id,
1993 $texts->{description} = $translation->translation if $translation && $translation->translation;
1994 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
2000 return $_[0]->type_data->nr_key;
2003 sub save_and_redirect_to {
2004 my ($self, %params) = @_;
2006 my $errors = $self->save();
2008 if (scalar @{ $errors }) {
2009 $self->js->flash('error', $_) foreach @{ $errors };
2010 return $self->js->render();
2013 flash_later('info', $self->type_data->text("saved"));
2015 $self->redirect_to(%params, id => $self->order->id);
2019 my ($self, $addition) = @_;
2021 my $number_type = $self->nr_key;
2022 my $snumbers = $number_type . '_' . $self->order->$number_type;
2024 SL::DB::History->new(
2025 trans_id => $self->order->id,
2026 employee_id => SL::DB::Manager::Employee->current->id,
2027 what_done => $self->order->type,
2028 snumbers => $snumbers,
2029 addition => $addition,
2033 sub store_pdf_to_webdav_and_filemanagement {
2034 my($order, $content, $filename) = @_;
2038 # copy file to webdav folder
2039 if ($order->number && $::instance_conf->get_webdav_documents) {
2040 my $webdav = SL::Webdav->new(
2041 type => $order->type,
2042 number => $order->number,
2044 my $webdav_file = SL::Webdav::File->new(
2046 filename => $filename,
2049 $webdav_file->store(data => \$content);
2052 push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
2055 if ($order->id && $::instance_conf->get_doc_storage) {
2057 SL::File->save(object_id => $order->id,
2058 object_type => $order->type,
2059 mime_type => 'application/pdf',
2060 source => 'created',
2061 file_type => 'document',
2062 file_name => $filename,
2063 file_contents => $content);
2066 push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2073 sub calculate_stock_in_out {
2074 my ($self, $item) = @_;
2076 return "" if !$item->part || !$item->part->unit || !$item->unit;
2078 my $in_out = $self->type_data->transfer;
2080 my $do_qty = $item->qty;
2081 my $sum = sum0 map {
2082 $_->unit_obj->convert_to($_->qty, $item->unit_obj)
2083 } $item->delivery_order_stock_entries;
2085 my $matches = $do_qty == $sum;
2086 my $content = _format_number_units($sum, 2, $item->unit_obj, $item->part->unit_obj);
2091 sub init_type_data {
2092 SL::Controller::DeliveryOrder::TypeData->new($_[0]);
2095 sub init_valid_types {
2096 $_[0]->type_data->valid_types;
2107 SL::Controller::Order - controller for orders
2111 This is a new form to enter orders, completely rewritten with the use
2112 of controller and java script techniques.
2114 The aim is to provide the user a better experience and a faster workflow. Also
2115 the code should be more readable, more reliable and better to maintain.
2123 One input row, so that input happens every time at the same place.
2127 Use of pickers where possible.
2131 Possibility to enter more than one item at once.
2135 Item list in a scrollable area, so that the workflow buttons stay at
2140 Reordering item rows with drag and drop is possible. Sorting item rows is
2141 possible (by partnumber, description, qty, sellprice and discount for now).
2145 No C<update> is necessary. All entries and calculations are managed
2146 with ajax-calls and the page only reloads on C<save>.
2150 User can see changes immediately, because of the use of java script
2161 =item * C<SL/Controller/Order.pm>
2165 =item * C<template/webpages/delivery_order/form.html>
2169 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
2171 Main tab for basic_data.
2173 This is the only tab here for now. "linked records" and "webdav" tabs are
2174 reused from generic code.
2178 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
2180 For displaying information on business type
2182 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
2184 The input line for items
2186 =item * C<template/webpages/delivery_order/tabs/_row.html>
2188 One row for already entered items
2190 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
2192 Displaying tax information
2196 =item * C<js/kivi.DeliveryOrder.js>
2198 java script functions
2208 =item * price sources: little symbols showing better price / better discount
2210 =item * select units in input row?
2212 =item * check for direct delivery (workflow sales order -> purchase order)
2214 =item * access rights
2216 =item * display weights
2220 =item * optional client/user behaviour
2222 (transactions has to be set - department has to be set -
2223 force project if enabled in client config - transport cost reminder)
2227 =head1 KNOWN BUGS AND CAVEATS
2233 Customer discount is not displayed as a valid discount in price source popup
2234 (this might be a bug in price sources)
2236 (I cannot reproduce this (Bernd))
2240 No indication that <shift>-up/down expands/collapses second row.
2244 Inline creation of parts is not currently supported
2248 Table header is not sticky in the scrolling area.
2252 Sorting does not include C<position>, neither does reordering.
2254 This behavior was implemented intentionally. But we can discuss, which behavior
2255 should be implemented.
2259 =head1 To discuss / Nice to have
2265 How to expand/collapse second row. Now it can be done clicking the icon or
2270 Possibility to select PriceSources in input row?
2274 This controller uses a (changed) copy of the template for the PriceSource
2275 dialog. Maybe there could be used one code source.
2279 Rounding-differences between this controller (PriceTaxCalculator) and the old
2280 form. This is not only a problem here, but also in all parts using the PTC.
2281 There exists a ticket and a patch. This patch should be testet.
2285 An indicator, if the actual inputs are saved (like in an
2286 editor or on text processing application).
2290 A warning when leaving the page without saveing unchanged inputs.
2297 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>