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);
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');
58 __PACKAGE__->run_before('get_unalterable_data',
59 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
70 $self->order->transdate(DateTime->now_local());
71 $self->type_data->set_reqdate_by_type;
76 'delivery_order/form',
77 title => $self->get_title_for('add'),
78 %{$self->{template_args}}
82 sub action_add_from_order {
84 # this interfers with init_order
85 $self->{converted_from_oe_id} = delete $::form->{id};
87 # TODO copy data and remember to link them on save
92 # edit an existing order
100 # this is to edit an order from an unsaved order object
102 # set item ids to new fake id, to identify them as new items
103 foreach my $item (@{$self->order->items_sorted}) {
104 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
106 # trigger rendering values for second row as hidden, because they
107 # are loaded only on demand. So we need to keep the values from
109 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
114 'delivery_order/form',
115 title => $self->get_title_for('edit'),
116 %{$self->{template_args}}
120 # edit a collective order (consisting of one or more existing orders)
121 sub action_edit_collective {
125 my @multi_ids = map {
126 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
127 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
129 # fall back to add if no ids are given
130 if (scalar @multi_ids == 0) {
135 # fall back to save as new if only one id is given
136 if (scalar @multi_ids == 1) {
137 $self->order(SL::DB::DeliveryOrder->new(id => $multi_ids[0])->load);
138 $self->action_save_as_new();
142 # make new order from given orders
143 my @multi_orders = map { SL::DB::DeliveryOrder->new(id => $_)->load } @multi_ids;
144 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
145 $self->order(SL::DB::DeliveryOrder->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
147 $self->action_edit();
154 my $errors = $self->delete();
156 if (scalar @{ $errors }) {
157 $self->js->flash('error', $_) foreach @{ $errors };
158 return $self->js->render();
161 flash_later('info', $self->type_data->text("delete"));
163 my @redirect_params = (
168 $self->redirect_to(@redirect_params);
175 my $errors = $self->save();
177 if (scalar @{ $errors }) {
178 $self->js->flash('error', $_) foreach @{ $errors };
179 return $self->js->render();
182 flash_later('info', $self->type_data->text("saved"));
184 my @redirect_params = (
187 id => $self->order->id,
190 $self->redirect_to(@redirect_params);
193 # save the order as new document an open it for edit
194 sub action_save_as_new {
197 my $order = $self->order;
200 $self->js->flash('error', t8('This object has not been saved yet.'));
201 return $self->js->render();
204 # load order from db to check if values changed
205 my $saved_order = SL::DB::DeliveryOrder->new(id => $order->id)->load;
208 # Lets assign a new number if the user hasn't changed the previous one.
209 # If it has been changed manually then use it as-is.
210 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
212 : trim($order->number);
214 # Clear transdate unless changed
215 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
216 ? DateTime->today_local
219 # Set new reqdate unless changed if it is enabled in client config
220 $new_attrs{reqdate} = $self->type_data->get_reqdate_by_type($order->reqdate, $saved_order->reqdate);
223 $new_attrs{employee} = SL::DB::Manager::Employee->current;
225 # Create new record from current one
226 $self->order(SL::DB::DeliveryOrder->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
228 # no linked records on save as new
229 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
232 $self->action_save();
237 # This is called if "print" is pressed in the print dialog.
238 # If PDF creation was requested and succeeded, the pdf is offered for download
239 # via send_file (which uses ajax in this case).
243 my $errors = $self->save();
245 if (scalar @{ $errors }) {
246 $self->js->flash('error', $_) foreach @{ $errors };
247 return $self->js->render();
250 $self->js_reset_order_and_item_ids_after_save;
252 my $format = $::form->{print_options}->{format};
253 my $media = $::form->{print_options}->{media};
254 my $formname = $::form->{print_options}->{formname};
255 my $copies = $::form->{print_options}->{copies};
256 my $groupitems = $::form->{print_options}->{groupitems};
257 my $printer_id = $::form->{print_options}->{printer_id};
259 # only pdf and opendocument by now
260 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
261 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
264 # only screen or printer by now
265 if (none { $media eq $_ } qw(screen printer)) {
266 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
269 # create a form for generate_attachment_filename
270 my $form = Form->new;
271 $form->{$self->nr_key()} = $self->order->number;
272 $form->{type} = $self->type;
273 $form->{format} = $format;
274 $form->{formname} = $formname;
275 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
276 my $pdf_filename = $form->generate_attachment_filename();
279 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
280 formname => $formname,
281 language => $self->order->language,
282 printer_id => $printer_id,
283 groupitems => $groupitems });
284 if (scalar @errors) {
285 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
288 if ($media eq 'screen') {
290 $self->js->flash('info', t8('The PDF has been created'));
293 type => SL::MIME->mime_type_from_ext($pdf_filename),
294 name => $pdf_filename,
298 } elsif ($media eq 'printer') {
300 my $printer_id = $::form->{print_options}->{printer_id};
301 SL::DB::Printer->new(id => $printer_id)->load->print_document(
306 $self->js->flash('info', t8('The PDF has been printed'));
309 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $pdf_filename);
310 if (scalar @warnings) {
311 $self->js->flash('warning', $_) for @warnings;
314 $self->save_history('PRINTED');
317 ->run('kivi.ActionBar.setEnabled', '#save_and_email_action')
320 sub action_preview_pdf {
323 my $errors = $self->save();
324 if (scalar @{ $errors }) {
325 $self->js->flash('error', $_) foreach @{ $errors };
326 return $self->js->render();
329 $self->js_reset_order_and_item_ids_after_save;
332 my $media = 'screen';
333 my $formname = $self->type;
336 # create a form for generate_attachment_filename
337 my $form = Form->new;
338 $form->{$self->nr_key()} = $self->order->number;
339 $form->{type} = $self->type;
340 $form->{format} = $format;
341 $form->{formname} = $formname;
342 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
343 my $pdf_filename = $form->generate_attachment_filename();
346 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
347 formname => $formname,
348 language => $self->order->language,
350 if (scalar @errors) {
351 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
353 $self->save_history('PREVIEWED');
354 $self->js->flash('info', t8('The PDF has been previewed'));
358 type => SL::MIME->mime_type_from_ext($pdf_filename),
359 name => $pdf_filename,
364 # open the email dialog
365 sub action_save_and_show_email_dialog {
368 my $errors = $self->save();
370 if (scalar @{ $errors }) {
371 $self->js->flash('error', $_) foreach @{ $errors };
372 return $self->js->render();
375 my $cv_method = $self->cv;
377 if (!$self->order->$cv_method) {
378 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'))
383 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
384 $email_form->{to} ||= $self->order->$cv_method->email;
385 $email_form->{cc} = $self->order->$cv_method->cc;
386 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
387 # Todo: get addresses from shipto, if any
389 my $form = Form->new;
390 $form->{$self->nr_key()} = $self->order->number;
391 $form->{cusordnumber} = $self->order->cusordnumber;
392 $form->{formname} = $self->type;
393 $form->{type} = $self->type;
394 $form->{language} = '_' . $self->order->language->template_code if $self->order->language;
395 $form->{language_id} = $self->order->language->id if $self->order->language;
396 $form->{format} = 'pdf';
397 $form->{cp_id} = $self->order->contact->cp_id if $self->order->contact;
399 $email_form->{subject} = $form->generate_email_subject();
400 $email_form->{attachment_filename} = $form->generate_attachment_filename();
401 $email_form->{message} = $form->generate_email_body();
402 $email_form->{js_send_function} = 'kivi.Order.send_email()';
404 my %files = $self->get_files_for_email_dialog();
405 $self->{all_employees} = SL::DB::Manager::Employee->get_all(query => [ deleted => 0 ]);
406 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
407 email_form => $email_form,
408 show_bcc => $::auth->assert('email_bcc', 'may fail'),
410 is_customer => $self->type_data->is_customer,
411 ALL_EMPLOYEES => $self->{all_employees},
415 ->run('kivi.Order.show_email_dialog', $dialog_html)
422 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
423 sub action_send_email {
426 my $errors = $self->save();
428 if (scalar @{ $errors }) {
429 $self->js->run('kivi.Order.close_email_dialog');
430 $self->js->flash('error', $_) foreach @{ $errors };
431 return $self->js->render();
434 $self->js_reset_order_and_item_ids_after_save;
436 my $email_form = delete $::form->{email_form};
437 my %field_names = (to => 'email');
439 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
441 # for Form::cleanup which may be called in Form::send_email
442 $::form->{cwd} = getcwd();
443 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
445 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
446 $::form->{media} = 'email';
448 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
450 my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
451 format => $::form->{print_options}->{format},
452 formname => $::form->{print_options}->{formname},
453 language => $self->order->language,
454 printer_id => $::form->{print_options}->{printer_id},
455 groupitems => $::form->{print_options}->{groupitems}});
456 if (scalar @errors) {
457 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
460 my @warnings = store_pdf_to_webdav_and_filemanagement($self->order, $pdf, $::form->{attachment_filename});
461 if (scalar @warnings) {
462 flash_later('warning', $_) for @warnings;
465 my $sfile = SL::SessionFile::Random->new(mode => "w");
466 $sfile->fh->print($pdf);
469 $::form->{tmpfile} = $sfile->file_name;
470 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
473 $::form->{id} = $self->order->id; # this is used in SL::Mailer to create a linked record to the mail
474 $::form->send_email(\%::myconfig, 'pdf');
477 my $intnotes = $self->order->intnotes;
478 $intnotes .= "\n\n" if $self->order->intnotes;
479 $intnotes .= t8('[email]') . "\n";
480 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
481 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
482 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
483 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
484 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
485 $intnotes .= t8('Message') . ": " . $::form->{message};
487 $self->order->update_attributes(intnotes => $intnotes);
489 $self->save_history('MAILED');
491 flash_later('info', t8('The email has been sent.'));
493 my @redirect_params = (
496 id => $self->order->id,
499 $self->redirect_to(@redirect_params);
502 # save the order and redirect to the frontend subroutine for a new
504 sub action_save_and_delivery_order {
507 $self->save_and_redirect_to(
508 controller => 'oe.pl',
509 action => 'oe_delivery_order_from_order',
513 # save the order and redirect to the frontend subroutine for a new
515 sub action_save_and_invoice {
518 $self->save_and_redirect_to(
519 controller => 'oe.pl',
520 action => 'oe_invoice_from_order',
524 # workflow from sales order to sales quotation
525 sub action_sales_quotation {
526 $_[0]->workflow_sales_or_request_for_quotation();
529 # workflow from sales order to sales quotation
530 sub action_request_for_quotation {
531 $_[0]->workflow_sales_or_request_for_quotation();
534 # workflow from sales quotation to sales order
535 sub action_sales_order {
536 $_[0]->workflow_sales_or_purchase_order();
539 # workflow from rfq to purchase order
540 sub action_purchase_order {
541 $_[0]->workflow_sales_or_purchase_order();
544 # workflow from purchase order to ap transaction
545 sub action_save_and_ap_transaction {
548 $self->save_and_redirect_to(
549 controller => 'ap.pl',
550 action => 'add_from_purchase_order',
554 # set form elements in respect to a changed customer or vendor
556 # This action is called on an change of the customer/vendor picker.
557 sub action_customer_vendor_changed {
560 setup_order_from_cv($self->order);
562 my $cv_method = $self->cv;
564 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
565 $self->js->show('#cp_row');
567 $self->js->hide('#cp_row');
570 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
571 $self->js->show('#shipto_selection');
573 $self->js->hide('#shipto_selection');
576 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
579 ->replaceWith('#order_cp_id', $self->build_contact_select)
580 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
581 ->replaceWith('#shipto_inputs ', $self->build_shipto_inputs)
582 ->replaceWith('#business_info_row', $self->build_business_info_row)
583 ->val( '#order_taxzone_id', $self->order->taxzone_id)
584 ->val( '#order_taxincluded', $self->order->taxincluded)
585 ->val( '#order_currency_id', $self->order->currency_id)
586 ->val( '#order_payment_id', $self->order->payment_id)
587 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
588 ->val( '#order_intnotes', $self->order->intnotes)
589 ->val( '#order_language_id', $self->order->$cv_method->language_id)
590 ->focus( '#order_' . $self->cv . '_id')
591 ->run('kivi.Order.update_exchangerate');
593 $self->js_redisplay_cvpartnumbers;
597 # open the dialog for customer/vendor details
598 sub action_show_customer_vendor_details_dialog {
601 my $is_customer = 'customer' eq $::form->{vc};
604 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
606 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
609 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
610 $details{discount_as_percent} = $cv->discount_as_percent;
611 $details{creditlimt} = $cv->creditlimit_as_number;
612 $details{business} = $cv->business->description if $cv->business;
613 $details{language} = $cv->language_obj->description if $cv->language_obj;
614 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
615 $details{payment_terms} = $cv->payment->description if $cv->payment;
616 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
618 foreach my $entry (@{ $cv->shipto }) {
619 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
621 foreach my $entry (@{ $cv->contacts }) {
622 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
625 $_[0]->render('common/show_vc_details', { layout => 0 },
626 is_customer => $is_customer,
631 # called if a unit in an existing item row is changed
632 sub action_unit_changed {
635 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
636 my $item = $self->order->items_sorted->[$idx];
638 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
639 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
642 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
643 $self->js_redisplay_line_values;
647 # add an item row for a new item entered in the input row
648 sub action_add_item {
651 delete $::form->{add_item}->{create_part_type};
653 my $form_attr = $::form->{add_item};
655 return unless $form_attr->{parts_id};
657 my $item = new_item($self->order, $form_attr);
659 $self->order->add_items($item);
661 $self->get_item_cvpartnumber($item);
663 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
664 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
668 in_out => $self->type_data->transfer,
671 if ($::form->{insert_before_item_id}) {
673 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
676 ->append('#row_table_id', $row_as_html);
679 if ( $item->part->is_assortment ) {
680 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
681 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
682 my $attr = { parts_id => $assortment_item->parts_id,
683 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
684 unit => $assortment_item->unit,
685 description => $assortment_item->part->description,
687 my $item = new_item($self->order, $attr);
689 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
690 $item->discount(1) unless $assortment_item->charge;
692 $self->order->add_items( $item );
693 $self->get_item_cvpartnumber($item);
694 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
695 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
700 if ($::form->{insert_before_item_id}) {
702 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
705 ->append('#row_table_id', $row_as_html);
711 ->val('.add_item_input', '')
712 ->run('kivi.Order.init_row_handlers')
713 ->run('kivi.Order.renumber_positions')
714 ->focus('#add_item_parts_id_name');
716 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
721 # add item rows for multiple items at once
722 sub action_add_multi_items {
725 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
726 return $self->js->render() unless scalar @form_attr;
729 foreach my $attr (@form_attr) {
730 my $item = new_item($self->order, $attr);
732 if ( $item->part->is_assortment ) {
733 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
734 my $attr = { parts_id => $assortment_item->parts_id,
735 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
736 unit => $assortment_item->unit,
737 description => $assortment_item->part->description,
739 my $item = new_item($self->order, $attr);
741 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
742 $item->discount(1) unless $assortment_item->charge;
747 $self->order->add_items(@items);
749 foreach my $item (@items) {
750 $self->get_item_cvpartnumber($item);
751 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
752 my $row_as_html = $self->p->render('delivery_order/tabs/_row',
756 in_out => $self->type_data->transfer,
759 if ($::form->{insert_before_item_id}) {
761 ->before ('.row_entry:has(#item_' . $::form->{insert_before_item_id} . ')', $row_as_html);
764 ->append('#row_table_id', $row_as_html);
769 ->run('kivi.Part.close_picker_dialogs')
770 ->run('kivi.Order.init_row_handlers')
771 ->run('kivi.Order.renumber_positions')
772 ->focus('#add_item_parts_id_name');
774 $self->js->run('kivi.Order.row_table_scroll_down') if !$::form->{insert_before_item_id};
779 sub action_update_exchangerate {
783 is_standard => $self->order->currency_id == $::instance_conf->get_currency_id,
784 currency_name => $self->order->currency->name,
787 $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 });
790 # redisplay item rows if they are sorted by an attribute
791 sub action_reorder_items {
795 partnumber => sub { $_[0]->part->partnumber },
796 description => sub { $_[0]->description },
797 qty => sub { $_[0]->qty },
798 sellprice => sub { $_[0]->sellprice },
799 discount => sub { $_[0]->discount },
800 cvpartnumber => sub { $_[0]->{cvpartnumber} },
803 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
805 my $method = $sort_keys{$::form->{order_by}};
806 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
807 if ($::form->{sort_dir}) {
808 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
809 @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
811 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
814 if ( $::form->{order_by} =~ m/qty|sellprice|discount/ ){
815 @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
817 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
821 ->run('kivi.Order.redisplay_items', \@to_sort)
825 # show the popup to choose a price/discount source
826 sub action_price_popup {
829 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
830 my $item = $self->order->items_sorted->[$idx];
832 $self->render_price_dialog($item);
835 # save the order in a session variable and redirect to the part controller
836 sub action_create_part {
839 my $previousform = $::auth->save_form_in_session(non_scalars => 1);
841 my $callback = $self->url_for(
842 action => 'return_from_create_part',
843 type => $self->type, # type is needed for check_auth on return
844 previousform => $previousform,
847 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.'));
849 my @redirect_params = (
850 controller => 'Part',
852 part_type => $::form->{add_item}->{create_part_type},
853 callback => $callback,
857 $self->redirect_to(@redirect_params);
860 sub action_return_from_create_part {
863 $self->{created_part} = SL::DB::Part->new(id => delete $::form->{new_parts_id})->load if $::form->{new_parts_id};
865 $::auth->restore_form_from_session(delete $::form->{previousform});
867 # set item ids to new fake id, to identify them as new items
868 foreach my $item (@{$self->order->items_sorted}) {
869 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
872 $self->get_unalterable_data();
875 # trigger rendering values for second row/longdescription as hidden,
876 # because they are loaded only on demand. So we need to keep the values
878 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
879 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
882 'delivery_order/form',
883 title => $self->get_title_for('edit'),
884 %{$self->{template_args}}
889 # load the second row for one or more items
891 # This action gets the html code for all items second rows by rendering a template for
892 # the second row and sets the html code via client js.
893 sub action_load_second_rows {
896 foreach my $item_id (@{ $::form->{item_ids} }) {
897 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
898 my $item = $self->order->items_sorted->[$idx];
900 $self->js_load_second_row($item, $item_id, 0);
903 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
908 # update description, notes and sellprice from master data
909 sub action_update_row_from_master_data {
912 foreach my $item_id (@{ $::form->{item_ids} }) {
913 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
914 my $item = $self->order->items_sorted->[$idx];
915 my $texts = get_part_texts($item->part, $self->order->language_id);
917 $item->description($texts->{description});
918 $item->longdescription($texts->{longdescription});
920 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
923 if ($item->part->is_assortment) {
924 # add assortment items with price 0, as the components carry the price
925 $price_src = $price_source->price_from_source("");
926 $price_src->price(0);
928 $price_src = $price_source->best_price
929 ? $price_source->best_price
930 : $price_source->price_from_source("");
931 $price_src->price($::form->round_amount($price_src->price / $self->order->exchangerate, 5)) if $self->order->exchangerate;
932 $price_src->price(0) if !$price_source->best_price;
936 $item->sellprice($price_src->price);
937 $item->active_price_source($price_src);
940 ->run('kivi.Order.update_sellprice', $item_id, $item->sellprice_as_number)
941 ->html('.row_entry:has(#item_' . $item_id . ') [name = "partnumber"] a', $item->part->partnumber)
942 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].description"]', $item->description)
943 ->val ('.row_entry:has(#item_' . $item_id . ') [name = "order.orderitems[].longdescription"]', $item->longdescription);
945 if ($self->search_cvpartnumber) {
946 $self->get_item_cvpartnumber($item);
947 $self->js->html('.row_entry:has(#item_' . $item_id . ') [name = "cvpartnumber"]', $item->{cvpartnumber});
951 $self->js_redisplay_line_values;
956 sub action_transfer_stock {
959 if ($self->order->delivered) {
960 return $self->js->flash("error", t8('The parts for this order have already been transferred'))->render;
963 my $errors = $self->save;
966 $self->js->flash('error', $_) for @$errors;
967 return $self->js->render;
970 my $order = $self->order;
972 # TODO move to type data
973 my $trans_type = $self->type_data->properties('transfer') eq 'in'
974 ? SL::DB::Manager::TransferType->find_by(direction => "id", description => "stock")
975 : SL::DB::Manager::TransferType->find_by(direction => "out", deescription => "shipped");
977 my @transfer_requests;
979 for my $item (@{ $order->items_sorted }) {
980 for my $stock (@{ $item->delivery_order_stock_entries }) {
981 my $transfer = SL::DB::Inventory->new_from($stock);
982 $transfer->trans_type($trans_type);
984 push @transfer_requests, $transfer;
988 if (!@transfer_requests) {
989 $self->js->flash("error", t8("No stock to transfer"))->render;
992 SL::DB->with_transaction(sub {
993 $_->save for @transfer_requests;
994 $self->order->update_attributes(deliverd => 1);
997 $self->js->flash("info", t8("Stock transfered"))->render;
1000 sub js_load_second_row {
1001 my ($self, $item, $item_id, $do_parse) = @_;
1004 # Parse values from form (they are formated while rendering (template)).
1005 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1006 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
1007 foreach my $var (@{ $item->cvars_by_config }) {
1008 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1010 $item->parse_custom_variable_values;
1013 my $row_as_html = $self->p->render('delivery_order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
1016 ->html('#second_row_' . $item_id, $row_as_html)
1017 ->data('#second_row_' . $item_id, 'loaded', 1);
1020 sub js_redisplay_line_values {
1023 my $is_sales = $self->order->is_sales;
1025 # sales orders with margins
1030 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1031 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1032 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1033 ]} @{ $self->order->items_sorted };
1037 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1038 ]} @{ $self->order->items_sorted };
1042 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1045 sub js_redisplay_cvpartnumbers {
1048 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1050 my @data = map {[$_->{cvpartnumber}]} @{ $self->order->items_sorted };
1053 ->run('kivi.Order.redisplay_cvpartnumbers', \@data);
1056 sub js_reset_order_and_item_ids_after_save {
1060 ->val('#id', $self->order->id)
1061 ->val('#converted_from_oe_id', '')
1062 ->val('#order_' . $self->nr_key(), $self->order->number);
1065 foreach my $form_item_id (@{ $::form->{orderitem_ids} }) {
1066 next if !$self->order->items_sorted->[$idx]->id;
1067 next if $form_item_id !~ m{^new};
1069 ->val ('[name="orderitem_ids[+]"][value="' . $form_item_id . '"]', $self->order->items_sorted->[$idx]->id)
1070 ->val ('#item_' . $form_item_id, $self->order->items_sorted->[$idx]->id)
1071 ->attr('#item_' . $form_item_id, "id", 'item_' . $self->order->items_sorted->[$idx]->id);
1075 $self->js->val('[name="converted_from_orderitems_ids[+]"]', '');
1085 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1086 die "Not a valid type for delivery order";
1089 $self->type($::form->{type});
1095 return $self->type_data->customervendor;
1098 sub init_search_cvpartnumber {
1101 my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
1102 my $search_cvpartnumber;
1103 $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
1104 $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
1106 return $search_cvpartnumber;
1109 sub init_show_update_button {
1112 !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
1123 sub init_all_price_factors {
1124 SL::DB::Manager::PriceFactor->get_all;
1127 sub init_part_picker_classification_ids {
1130 return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(where => $self->type_data->part_classification_query) } ];
1136 $::auth->assert($self->type_data->access || 'DOES_NOT_EXIST');
1139 # build the selection box for contacts
1141 # Needed, if customer/vendor changed.
1142 sub build_contact_select {
1145 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1146 value_key => 'cp_id',
1147 title_key => 'full_name_dep',
1148 default => $self->order->cp_id,
1150 style => 'width: 300px',
1154 # build the selection box for shiptos
1156 # Needed, if customer/vendor changed.
1157 sub build_shipto_select {
1160 select_tag('order.shipto_id',
1161 [ {displayable_id => t8("No/individual shipping address"), shipto_id => ''}, $self->order->{$self->cv}->shipto ],
1162 value_key => 'shipto_id',
1163 title_key => 'displayable_id',
1164 default => $self->order->shipto_id,
1166 style => 'width: 300px',
1170 # build the inputs for the cusom shipto dialog
1172 # Needed, if customer/vendor changed.
1173 sub build_shipto_inputs {
1176 my $content = $self->p->render('common/_ship_to_dialog',
1177 vc_obj => $self->order->customervendor,
1178 cs_obj => $self->order->custom_shipto,
1179 cvars => $self->order->custom_shipto->cvars_by_config,
1180 id_selector => '#order_shipto_id');
1182 div_tag($content, id => 'shipto_inputs');
1185 # render the info line for business
1187 # Needed, if customer/vendor changed.
1188 sub build_business_info_row
1190 $_[0]->p->render('delivery_order/tabs/_business_info_row', SELF => $_[0]);
1197 return if !$::form->{id};
1199 $self->order(SL::DB::DeliveryOrder->new(id => $::form->{id})->load);
1201 # Add an empty custom shipto to the order, so that the dialog can render the cvar inputs.
1202 # You need a custom shipto object to call cvars_by_config to get the cvars.
1203 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => [])) if !$self->order->custom_shipto;
1205 return $self->order;
1208 # load or create a new order object
1210 # And assign changes from the form to this object.
1211 # If the order is loaded from db, check if items are deleted in the form,
1212 # remove them form the object and collect them for removing from db on saving.
1213 # Then create/update items from form (via make_item) and add them.
1217 # add_items adds items to an order with no items for saving, but they cannot
1218 # be retrieved via items until the order is saved. Adding empty items to new
1219 # order here solves this problem.
1221 $order = SL::DB::DeliveryOrder->new(id => $::form->{id})->load(with => [ 'orderitems', 'orderitems.part' ]) if $::form->{id};
1222 $order ||= SL::DB::DeliveryOrder->new(orderitems => [], currency_id => $::instance_conf->get_currency_id(), order_type => $self->type_data->validate($::form->{type}));
1224 my $cv_id_method = $self->cv . '_id';
1225 if (!$::form->{id} && $::form->{$cv_id_method}) {
1226 $order->$cv_id_method($::form->{$cv_id_method});
1227 setup_order_from_cv($order);
1230 my $form_orderitems = delete $::form->{order}->{orderitems};
1232 $order->assign_attributes(%{$::form->{order}});
1234 $self->setup_custom_shipto_from_form($order, $::form);
1236 # remove deleted items
1237 $self->item_ids_to_delete([]);
1238 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1239 my $item = $order->orderitems->[$idx];
1240 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1241 splice @{$order->orderitems}, $idx, 1;
1242 push @{$self->item_ids_to_delete}, $item->id;
1248 foreach my $form_attr (@{$form_orderitems}) {
1249 my $item = make_item($order, $form_attr);
1250 $item->position($pos);
1254 $order->add_items(grep {!$_->id} @items);
1259 # create or update items from form
1261 # Make item objects from form values. For items already existing read from db.
1262 # Create a new item else. And assign attributes.
1264 my ($record, $attr) = @_;
1267 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1269 my $is_new = !$item;
1271 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1272 # they cannot be retrieved via custom_variables until the order/orderitem is
1273 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1274 $item ||= SL::DB::DeliveryOrderItem->new(custom_variables => []);
1276 $item->assign_attributes(%$attr);
1279 my $texts = get_part_texts($item->part, $record->language_id);
1280 $item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
1281 $item->project_id($record->globalproject_id) if !defined $attr->{project_id};
1282 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
1290 # This is used to add one item
1292 my ($record, $attr) = @_;
1294 my $item = SL::DB::DeliveryOrderItem->new;
1296 # Remove attributes where the user left or set the inputs empty.
1297 # So these attributes will be undefined and we can distinguish them
1298 # from zero later on.
1299 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1300 delete $attr->{$_} if $attr->{$_} eq '';
1303 $item->assign_attributes(%$attr);
1305 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1306 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1308 $item->unit($part->unit) if !$item->unit;
1311 if ( $part->is_assortment ) {
1312 # add assortment items with price 0, as the components carry the price
1313 $price_src = $price_source->price_from_source("");
1314 $price_src->price(0);
1315 } elsif (defined $item->sellprice) {
1316 $price_src = $price_source->price_from_source("");
1317 $price_src->price($item->sellprice);
1319 $price_src = $price_source->best_price
1320 ? $price_source->best_price
1321 : $price_source->price_from_source("");
1322 $price_src->price(0) if !$price_source->best_price;
1326 if (defined $item->discount) {
1327 $discount_src = $price_source->discount_from_source("");
1328 $discount_src->discount($item->discount);
1330 $discount_src = $price_source->best_discount
1331 ? $price_source->best_discount
1332 : $price_source->discount_from_source("");
1333 $discount_src->discount(0) if !$price_source->best_discount;
1337 $new_attr{part} = $part;
1338 $new_attr{description} = $part->description if ! $item->description;
1339 $new_attr{qty} = 1.0 if ! $item->qty;
1340 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1341 $new_attr{sellprice} = $price_src->price;
1342 $new_attr{discount} = $discount_src->discount;
1343 $new_attr{active_price_source} = $price_src;
1344 $new_attr{active_discount_source} = $discount_src;
1345 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1346 $new_attr{project_id} = $record->globalproject_id;
1347 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1349 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1350 # they cannot be retrieved via custom_variables until the order/orderitem is
1351 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1352 $new_attr{custom_variables} = [];
1354 my $texts = get_part_texts($part, $record->language_id, description => $new_attr{description}, longdescription => $new_attr{longdescription});
1356 $item->assign_attributes(%new_attr, %{ $texts });
1361 sub setup_order_from_cv {
1364 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
1366 $order->intnotes($order->customervendor->notes);
1368 if ($order->is_sales) {
1369 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1370 $order->taxincluded(defined($order->customer->taxincluded_checked)
1371 ? $order->customer->taxincluded_checked
1372 : $::myconfig{taxincluded_checked});
1377 # setup custom shipto from form
1379 # The dialog returns form variables starting with 'shipto' and cvars starting
1380 # with 'shiptocvar_'.
1381 # Mark it to be deleted if a shipto from master data is selected
1382 # (i.e. order has a shipto).
1383 # Else, update or create a new custom shipto. If the fields are empty, it
1384 # will not be saved on save.
1385 sub setup_custom_shipto_from_form {
1386 my ($self, $order, $form) = @_;
1388 if ($order->shipto) {
1389 $self->is_custom_shipto_to_delete(1);
1391 my $custom_shipto = $order->custom_shipto || $order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1393 my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
1394 my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
1396 $custom_shipto->assign_attributes(%$shipto_attrs);
1397 $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
1401 # get data for saving, printing, ..., that is not changed in the form
1403 # Only cvars for now.
1404 sub get_unalterable_data {
1407 foreach my $item (@{ $self->order->items }) {
1408 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1409 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1410 foreach my $var (@{ $item->cvars_by_config }) {
1411 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1413 $item->parse_custom_variable_values;
1419 # And remove related files in the spool directory
1424 my $db = $self->order->db;
1426 $db->with_transaction(
1428 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1429 $self->order->delete;
1430 my $spool = $::lx_office_conf{paths}->{spool};
1431 unlink map { "$spool/$_" } @spoolfiles if $spool;
1433 $self->save_history('DELETED');
1436 }) || push(@{$errors}, $db->error);
1443 # And delete items that are deleted in the form.
1448 my $db = $self->order->db;
1450 $db->with_transaction(sub {
1451 # delete custom shipto if it is to be deleted or if it is empty
1452 if ($self->order->custom_shipto && ($self->is_custom_shipto_to_delete || $self->order->custom_shipto->is_empty)) {
1453 $self->order->custom_shipto->delete if $self->order->custom_shipto->shipto_id;
1454 $self->order->custom_shipto(undef);
1457 SL::DB::DeliveryOrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1458 $self->order->save(cascade => 1);
1461 if ($::form->{converted_from_oe_id}) {
1462 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1463 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1464 my $src = SL::DB::DeliveryOrder->new(id => $converted_from_oe_id)->load;
1465 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1466 $src->link_to_record($self->order);
1468 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1470 foreach (@{ $self->order->items_sorted }) {
1471 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1473 SL::DB::RecordLink->new(from_table => 'orderitems',
1474 from_id => $from_id,
1475 to_table => 'orderitems',
1483 $self->save_history('SAVED');
1486 }) || push(@{$errors}, $db->error);
1491 sub workflow_sales_or_request_for_quotation {
1495 my $errors = $self->save();
1497 if (scalar @{ $errors }) {
1498 $self->js->flash('error', $_) for @{ $errors };
1499 return $self->js->render();
1502 my $destination_type = $self->type_data->workflow("to_quotation_type");
1504 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1505 $self->{converted_from_oe_id} = delete $::form->{id};
1507 # set item ids to new fake id, to identify them as new items
1508 foreach my $item (@{$self->order->items_sorted}) {
1509 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1513 $::form->{type} = $destination_type;
1514 $self->type($self->init_type);
1515 $self->cv ($self->init_cv);
1518 $self->get_unalterable_data();
1519 $self->pre_render();
1521 # trigger rendering values for second row as hidden, because they
1522 # are loaded only on demand. So we need to keep the values from the
1524 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1527 'delivery_order/form',
1528 title => $self->get_title_for('edit'),
1529 %{$self->{template_args}}
1533 sub workflow_sales_or_purchase_order {
1537 my $errors = $self->save();
1539 if (scalar @{ $errors }) {
1540 $self->js->flash('error', $_) foreach @{ $errors };
1541 return $self->js->render();
1544 my $destination_type = $self->type_data->workflow("to_order_type");
1546 # check for direct delivery
1547 # copy shipto in custom shipto (custom shipto will be copied by new_from() in case)
1549 if ($self->type_data->workflow("to_order_copy_shipto") && $::form->{use_shipto} && $self->order->shipto) {
1550 $custom_shipto = $self->order->shipto->clone('SL::DB::DeliveryOrder');
1553 $self->order(SL::DB::DeliveryOrder->new_from($self->order, destination_type => $destination_type));
1554 $self->{converted_from_oe_id} = delete $::form->{id};
1556 # set item ids to new fake id, to identify them as new items
1557 foreach my $item (@{$self->order->items_sorted}) {
1558 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1561 if ($self->type_data->workflow("to_order_copy_shipto")) {
1562 if ($::form->{use_shipto}) {
1563 $self->order->custom_shipto($custom_shipto) if $custom_shipto;
1565 # remove any custom shipto if not wanted
1566 $self->order->custom_shipto(SL::DB::Shipto->new(module => 'OE', custom_variables => []));
1571 $::form->{type} = $destination_type;
1572 $self->type($self->init_type);
1573 $self->cv ($self->init_cv);
1576 $self->get_unalterable_data();
1577 $self->pre_render();
1579 # trigger rendering values for second row as hidden, because they
1580 # are loaded only on demand. So we need to keep the values from the
1582 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1585 'delivery_order/form',
1586 title => $self->get_title_for('edit'),
1587 %{$self->{template_args}}
1594 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1595 $self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
1596 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1597 $self->{all_languages} = SL::DB::Manager::Language->get_all_sorted();
1598 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1601 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1604 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1606 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1607 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1608 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1609 $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
1611 my $print_form = Form->new('');
1612 $print_form->{type} = $self->type;
1613 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1614 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1615 form => $print_form,
1616 options => {dialog_name_prefix => 'print_options.',
1620 no_opendocument => 0,
1624 foreach my $item (@{$self->order->orderitems}) {
1625 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1626 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1627 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1630 if ($self->order->${\ $self->type_data->nr_key } && $::instance_conf->get_webdav) {
1631 my $webdav = SL::Webdav->new(
1632 type => $self->type,
1633 number => $self->order->number,
1635 my @all_objects = $webdav->get_all_objects;
1636 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1638 link => File::Spec->catfile($_->full_filedescriptor),
1642 $self->{template_args}{in_out} = $self->type_data->transfer;
1644 $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
1646 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.DeliveryOrder kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
1647 calculate_qty kivi.Validator follow_up show_history);
1648 $self->setup_edit_action_bar;
1651 sub setup_edit_action_bar {
1652 my ($self, %params) = @_;
1654 my $deletion_allowed = $self->type_data->show_menu("delete");
1656 for my $bar ($::request->layout->get('actionbar')) {
1661 call => [ 'kivi.DeliveryOrder.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1662 $::instance_conf->get_order_warn_no_deliverydate,
1667 call => [ 'kivi.DeliveryOrder.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1668 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1670 ], # end of combobox "Save"
1677 t8('Save and Quotation'),
1678 submit => [ '#order_form', { action => "DeliveryOrder/sales_quotation" } ],
1679 only_if => $self->type_data->show_menu("save_and_quotation"),
1683 submit => [ '#order_form', { action => "DeliveryOrder/request_for_quotation" } ],
1684 only_if => $self->type_data->show_menu("save_and_rfq"),
1687 t8('Save and Sales Order'),
1688 submit => [ '#order_form', { action => "DeliveryOrder/sales_order" } ],
1689 only_if => $self->type_data->show_menu("save_and_sales_order"),
1692 t8('Save and Purchase Order'),
1693 call => [ 'kivi.DeliveryOrder.purchase_order_check_for_direct_delivery' ],
1694 only_if => $self->type_data->show_menu("save_and_purchase_order"),
1697 t8('Save and Delivery Order'),
1698 call => [ 'kivi.DeliveryOrder.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1699 $::instance_conf->get_order_warn_no_deliverydate,
1701 only_if => $self->type_data->show_menu("save_and_delivery_order"),
1704 t8('Save and Invoice'),
1705 call => [ 'kivi.DeliveryOrder.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1706 only_if => $self->type_data->show_menu("save_and_invoice"),
1709 t8('Save and AP Transaction'),
1710 call => [ 'kivi.DeliveryOrder.save', 'save_and_ap_transaction', $::instance_conf->get_order_warn_duplicate_parts ],
1711 only_if => $self->type_data->show_menu("save_and_ap_transaction"),
1714 ], # end of combobox "Workflow"
1721 t8('Save and preview PDF'),
1722 call => [ 'kivi.DeliveryOrder.save', 'preview_pdf', $::instance_conf->get_order_warn_duplicate_parts,
1723 $::instance_conf->get_order_warn_no_deliverydate,
1727 t8('Save and print'),
1728 call => [ 'kivi.DeliveryOrder.show_print_options', $::instance_conf->get_order_warn_duplicate_parts,
1729 $::instance_conf->get_order_warn_no_deliverydate,
1733 t8('Save and E-mail'),
1734 id => 'save_and_email_action',
1735 call => [ 'kivi.DeliveryOrder.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts,
1736 $::instance_conf->get_order_warn_no_deliverydate,
1738 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1741 t8('Download attachments of all parts'),
1742 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1743 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1744 only_if => $::instance_conf->get_doc_storage,
1746 ], # end of combobox "Export"
1750 call => [ 'kivi.DeliveryOrder.delete_order' ],
1751 confirm => $::locale->text('Do you really want to delete this object?'),
1752 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1753 only_if => $self->type_data->show_menu("delete"),
1759 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1760 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1761 only_if => $self->type_data->properties('transfer') eq 'out',
1762 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1766 submit => [ '#order_form', { action => "DeliveryOrder/transfer_stock" } ],
1767 disabled => $self->order->delivered ? t8('The parts for this order have already been transferred') : undef,
1768 only_if => $self->type_data->properties('transfer') eq 'in',
1769 confirm => t8('Do you really want to transfer the stock and set this order to delivered?'),
1779 call => [ 'kivi.DeliveryOrder.follow_up_window' ],
1780 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1781 only_if => $::auth->assert('productivity', 1),
1785 call => [ 'set_history_window', $self->order->id, 'id' ],
1786 disabled => !$self->order->id ? t8('This record has not been saved yet.') : undef,
1788 ], # end of combobox "more"
1794 my ($order, $pdf_ref, $params) = @_;
1798 my $print_form = Form->new('');
1799 $print_form->{type} = $order->type;
1800 $print_form->{formname} = $params->{formname} || $order->type;
1801 $print_form->{format} = $params->{format} || 'pdf';
1802 $print_form->{media} = $params->{media} || 'file';
1803 $print_form->{groupitems} = $params->{groupitems};
1804 $print_form->{printer_id} = $params->{printer_id};
1805 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1807 $order->language($params->{language});
1808 $order->flatten_to_form($print_form, format_amounts => 1);
1812 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
1813 $template_ext = 'odt';
1814 $template_type = 'OpenDocument';
1817 # search for the template
1818 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1819 name => $print_form->{formname},
1820 extension => $template_ext,
1821 email => $print_form->{media} eq 'email',
1822 language => $params->{language},
1823 printer_id => $print_form->{printer_id},
1826 if (!defined $template_file) {
1827 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);
1830 return @errors if scalar @errors;
1832 $print_form->throw_on_error(sub {
1834 $print_form->prepare_for_printing;
1836 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1837 format => $print_form->{format},
1838 template_type => $template_type,
1839 template => $template_file,
1840 variables => $print_form,
1841 variable_content_types => {
1842 longdescription => 'html',
1843 partnotes => 'html',
1848 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->error : $EVAL_ERROR;
1854 sub get_files_for_email_dialog {
1857 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1859 return %files if !$::instance_conf->get_doc_storage;
1861 if ($self->order->id) {
1862 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1863 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1864 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1865 $files{project_files} = [ SL::File->get_all( object_id => $self->order->globalproject_id, object_type => 'project', file_type => 'attachment') ];
1869 uniq_by { $_->{id} }
1871 +{ id => $_->part->id,
1872 partnumber => $_->part->partnumber }
1873 } @{$self->order->items_sorted};
1875 foreach my $part (@parts) {
1876 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1877 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1880 foreach my $key (keys %files) {
1881 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1888 my ($self, $action) = @_;
1890 return '' if none { lc($action)} qw(add edit);
1891 return $self->type_data->text($action);
1894 sub get_item_cvpartnumber {
1895 my ($self, $item) = @_;
1897 return if !$self->search_cvpartnumber;
1898 return if !$self->order->customervendor;
1900 if ($self->cv eq 'vendor') {
1901 my @mms = grep { $_->make eq $self->order->customervendor->id } @{$item->part->makemodels};
1902 $item->{cvpartnumber} = $mms[0]->model if scalar @mms;
1903 } elsif ($self->cv eq 'customer') {
1904 my @cps = grep { $_->customer_id eq $self->order->customervendor->id } @{$item->part->customerprices};
1905 $item->{cvpartnumber} = $cps[0]->customer_partnumber if scalar @cps;
1909 sub get_part_texts {
1910 my ($part_or_id, $language_or_id, %defaults) = @_;
1912 my $part = ref($part_or_id) ? $part_or_id : SL::DB::Part->load_cached($part_or_id);
1913 my $language_id = ref($language_or_id) ? $language_or_id->id : $language_or_id;
1915 description => $defaults{description} // $part->description,
1916 longdescription => $defaults{longdescription} // $part->notes,
1919 return $texts unless $language_id;
1921 my $translation = SL::DB::Manager::Translation->get_first(
1923 parts_id => $part->id,
1924 language_id => $language_id,
1927 $texts->{description} = $translation->translation if $translation && $translation->translation;
1928 $texts->{longdescription} = $translation->longdescription if $translation && $translation->longdescription;
1934 return $_[0]->type_data->nr_key;
1937 sub save_and_redirect_to {
1938 my ($self, %params) = @_;
1940 my $errors = $self->save();
1942 if (scalar @{ $errors }) {
1943 $self->js->flash('error', $_) foreach @{ $errors };
1944 return $self->js->render();
1947 flash_later('info', $self->type_data->text("saved"));
1949 $self->redirect_to(%params, id => $self->order->id);
1953 my ($self, $addition) = @_;
1955 my $number_type = $self->nr_key;
1956 my $snumbers = $number_type . '_' . $self->order->$number_type;
1958 SL::DB::History->new(
1959 trans_id => $self->order->id,
1960 employee_id => SL::DB::Manager::Employee->current->id,
1961 what_done => $self->order->type,
1962 snumbers => $snumbers,
1963 addition => $addition,
1967 sub store_pdf_to_webdav_and_filemanagement {
1968 my($order, $content, $filename) = @_;
1972 # copy file to webdav folder
1973 if ($order->number && $::instance_conf->get_webdav_documents) {
1974 my $webdav = SL::Webdav->new(
1975 type => $order->type,
1976 number => $order->number,
1978 my $webdav_file = SL::Webdav::File->new(
1980 filename => $filename,
1983 $webdav_file->store(data => \$content);
1986 push @errors, t8('Storing PDF to webdav folder failed: #1', $@);
1989 if ($order->id && $::instance_conf->get_doc_storage) {
1991 SL::File->save(object_id => $order->id,
1992 object_type => $order->type,
1993 mime_type => 'application/pdf',
1994 source => 'created',
1995 file_type => 'document',
1996 file_name => $filename,
1997 file_contents => $content);
2000 push @errors, t8('Storing PDF in storage backend failed: #1', $@);
2007 sub calculate_stock_in_out {
2008 my ($self, $item) = @_;
2010 return "" if !$item->part || !$item->part->unit || !$item->unit;
2012 my $in_out = $self->type_data->transfer;
2014 my $do_qty = $item->qty;
2015 my $sum = sum0 map {
2016 $_->unit_obj->convert_to($_->qty, $item->unit_obj)
2017 } $item->delivery_order_stock_entries;
2019 my $matches = $do_qty == $sum;
2020 my $content = _format_number_units($sum, 2, $item->unit_obj, $item->part->unit_obj);
2025 sub init_type_data {
2026 SL::Controller::DeliveryOrder::TypeData->new($_[0]);
2029 sub init_valid_types {
2030 $_[0]->type_data->valid_types;
2041 SL::Controller::Order - controller for orders
2045 This is a new form to enter orders, completely rewritten with the use
2046 of controller and java script techniques.
2048 The aim is to provide the user a better experience and a faster workflow. Also
2049 the code should be more readable, more reliable and better to maintain.
2057 One input row, so that input happens every time at the same place.
2061 Use of pickers where possible.
2065 Possibility to enter more than one item at once.
2069 Item list in a scrollable area, so that the workflow buttons stay at
2074 Reordering item rows with drag and drop is possible. Sorting item rows is
2075 possible (by partnumber, description, qty, sellprice and discount for now).
2079 No C<update> is necessary. All entries and calculations are managed
2080 with ajax-calls and the page only reloads on C<save>.
2084 User can see changes immediately, because of the use of java script
2095 =item * C<SL/Controller/Order.pm>
2099 =item * C<template/webpages/delivery_order/form.html>
2103 =item * C<template/webpages/delivery_order/tabs/basic_data.html>
2105 Main tab for basic_data.
2107 This is the only tab here for now. "linked records" and "webdav" tabs are
2108 reused from generic code.
2112 =item * C<template/webpages/delivery_order/tabs/_business_info_row.html>
2114 For displaying information on business type
2116 =item * C<template/webpages/delivery_order/tabs/_item_input.html>
2118 The input line for items
2120 =item * C<template/webpages/delivery_order/tabs/_row.html>
2122 One row for already entered items
2124 =item * C<template/webpages/delivery_order/tabs/_tax_row.html>
2126 Displaying tax information
2130 =item * C<js/kivi.DeliveryOrder.js>
2132 java script functions
2142 =item * price sources: little symbols showing better price / better discount
2144 =item * select units in input row?
2146 =item * check for direct delivery (workflow sales order -> purchase order)
2148 =item * access rights
2150 =item * display weights
2154 =item * optional client/user behaviour
2156 (transactions has to be set - department has to be set -
2157 force project if enabled in client config - transport cost reminder)
2161 =head1 KNOWN BUGS AND CAVEATS
2167 Customer discount is not displayed as a valid discount in price source popup
2168 (this might be a bug in price sources)
2170 (I cannot reproduce this (Bernd))
2174 No indication that <shift>-up/down expands/collapses second row.
2178 Inline creation of parts is not currently supported
2182 Table header is not sticky in the scrolling area.
2186 Sorting does not include C<position>, neither does reordering.
2188 This behavior was implemented intentionally. But we can discuss, which behavior
2189 should be implemented.
2193 =head1 To discuss / Nice to have
2199 How to expand/collapse second row. Now it can be done clicking the icon or
2204 Possibility to select PriceSources in input row?
2208 This controller uses a (changed) copy of the template for the PriceSource
2209 dialog. Maybe there could be used one code source.
2213 Rounding-differences between this controller (PriceTaxCalculator) and the old
2214 form. This is not only a problem here, but also in all parts using the PTC.
2215 There exists a ticket and a patch. This patch should be testet.
2219 An indicator, if the actual inputs are saved (like in an
2220 editor or on text processing application).
2224 A warning when leaving the page without saveing unchanged inputs.
2231 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>