|
package SL::Controller::Order;
|
|
|
|
use strict;
|
|
use parent qw(SL::Controller::Base);
|
|
|
|
use SL::Helper::Flash qw(flash_later);
|
|
use SL::HTML::Util;
|
|
use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
|
|
use SL::Locale::String qw(t8);
|
|
use SL::SessionFile::Random;
|
|
use SL::IMAPClient;
|
|
use SL::PriceSource;
|
|
use SL::Webdav;
|
|
use SL::File;
|
|
use SL::MIME;
|
|
use SL::Util qw(trim);
|
|
use SL::YAML;
|
|
use SL::DB::AdditionalBillingAddress;
|
|
use SL::DB::AuthUser;
|
|
use SL::DB::History;
|
|
use SL::DB::Order;
|
|
use SL::DB::Default;
|
|
use SL::DB::Unit;
|
|
use SL::DB::Part;
|
|
use SL::DB::PartClassification;
|
|
use SL::DB::PartsGroup;
|
|
use SL::DB::Printer;
|
|
use SL::DB::Note;
|
|
use SL::DB::Language;
|
|
use SL::DB::Reclamation;
|
|
use SL::DB::RecordLink;
|
|
use SL::DB::RequirementSpec;
|
|
use SL::DB::Shipto;
|
|
use SL::DB::Translation;
|
|
use SL::DB::ValidityToken;
|
|
use SL::DB::EmailJournal;
|
|
use SL::DB::EmailJournalAttachment;
|
|
|
|
use SL::Helper::CreatePDF qw(:all);
|
|
use SL::Helper::PrintOptions;
|
|
use SL::Helper::ShippedQty;
|
|
use SL::Helper::UserPreferences::DisplayPreferences;
|
|
use SL::Helper::UserPreferences::PositionsScrollbar;
|
|
use SL::Helper::UserPreferences::UpdatePositions;
|
|
|
|
use SL::Controller::Helper::GetModels;
|
|
|
|
use List::Util qw(first sum0);
|
|
use List::UtilsBy qw(sort_by uniq_by);
|
|
use List::MoreUtils qw(uniq any none pairwise first_index);
|
|
use English qw(-no_match_vars);
|
|
use File::Spec;
|
|
use Cwd;
|
|
use Sort::Naturally;
|
|
|
|
use Rose::Object::MakeMethods::Generic
|
|
(
|
|
scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
|
|
'scalar --get_set_init' => [ qw(order valid_types type cv p all_price_factors
|
|
search_cvpartnumber show_update_button
|
|
part_picker_classification_ids
|
|
is_final_version) ],
|
|
);
|
|
|
|
|
|
# safety
|
|
__PACKAGE__->run_before('check_auth',
|
|
except => [ qw(close_quotations) ]);
|
|
|
|
__PACKAGE__->run_before('check_auth_for_edit',
|
|
except => [ qw(edit show_customer_vendor_details_dialog price_popup load_second_rows close_quotations) ]);
|
|
|
|
#
|
|
# actions
|
|
#
|
|
|
|
# add a new order
|
|
sub action_add {
|
|
my ($self) = @_;
|
|
|
|
$self->order->transdate(DateTime->now_local());
|
|
my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
|
|
$self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval :
|
|
$self->type eq sales_order_intake_type() ? $::instance_conf->get_delivery_date_interval : 1;
|
|
|
|
if (($self->type eq sales_order_intake_type() && $::instance_conf->get_deliverydate_on)
|
|
|| ($self->type eq sales_order_type() && $::instance_conf->get_deliverydate_on)
|
|
|| ($self->type eq sales_quotation_type() && $::instance_conf->get_reqdate_on)
|
|
&& (!$self->order->reqdate)) {
|
|
$self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
|
|
}
|
|
|
|
|
|
$self->pre_render();
|
|
|
|
if (!$::form->{form_validity_token}) {
|
|
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
|
|
}
|
|
|
|
$self->render(
|
|
'order/form',
|
|
title => $self->get_title_for('add'),
|
|
%{$self->{template_args}}
|
|
);
|
|
}
|
|
|
|
sub action_add_from_reclamation {
|
|
my ($self) = @_;
|
|
|
|
my $reclamation = SL::DB::Reclamation->new(id => $::form->{from_id})->load;
|
|
my %params;
|
|
$params{destination_type} = $reclamation->is_sales ? 'sales_order'
|
|
: 'purchase_order';
|
|
my $order = SL::DB::Order->new_from($reclamation, %params);
|
|
$self->{converted_from_reclamation_id} = $::form->{from_id};
|
|
|
|
$self->order($order);
|
|
|
|
$self->recalc();
|
|
$self->pre_render();
|
|
|
|
if (!$::form->{form_validity_token}) {
|
|
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
|
|
}
|
|
|
|
$self->render(
|
|
'order/form',
|
|
title => $self->get_title_for('edit'),
|
|
%{$self->{template_args}}
|
|
);
|
|
}
|
|
|
|
sub action_add_from_email_journal {
|
|
my ($self) = @_;
|
|
my $email_journal_id = $::form->{from_id};
|
|
my $email_attachment_id = $::form->{email_attachment_id};
|
|
|
|
$self->{converted_from_email_journal_id} = $email_journal_id;
|
|
$self->{email_attachment_id} = $email_attachment_id;
|
|
|
|
$self->action_add();
|
|
}
|
|
|
|
# edit an existing order
|
|
sub action_edit {
|
|
my ($self) = @_;
|
|
|
|
if ($::form->{id}) {
|
|
$self->load_order;
|
|
|
|
if ($self->order->is_sales) {
|
|
my $imap_client = SL::IMAPClient->new();
|
|
if ($imap_client) {
|
|
$imap_client->update_email_files_for_record($self->order);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
# this is to edit an order from an unsaved order object
|
|
|
|
# set item ids to new fake id, to identify them as new items
|
|
foreach my $item (@{$self->order->items_sorted}) {
|
|
$item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
|
|
}
|
|
# trigger rendering values for second row as hidden, because they
|
|
# are loaded only on demand. So we need to keep the values from
|
|
# the source.
|
|
$_->{render_second_row} = 1 for @{ $self->order->items_sorted };
|
|
|
|
if (!$::form->{form_validity_token}) {
|
|
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
|
|
}
|
|
}
|
|
|
|
$self->recalc();
|
|
$self->pre_render();
|
|
$self->render(
|
|
'order/form',
|
|
title => $self->get_title_for('edit'),
|
|
%{$self->{template_args}}
|
|
);
|
|
}
|
|
|
|
# edit a collective order (consisting of one or more existing orders)
|
|
sub action_edit_collective {
|
|
my ($self) = @_;
|
|
|
|
# collect order ids
|
|
my @multi_ids = map {
|
|
$_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
|
|
} grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
|
|
|
|
# fall back to add if no ids are given
|
|
if (scalar @multi_ids == 0) {
|
|
$self->action_add();
|
|
return;
|
|
}
|
|
|
|
# fall back to save as new if only one id is given
|
|
if (scalar @multi_ids == 1) {
|
|
$self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
|
|
$self->action_save_as_new();
|
|
return;
|
|
}
|
|
|
|
# make new order from given orders
|
|
my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
|
|
$self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
|
|
$self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
|
|
|
|
$self->action_edit();
|
|
}
|
|
|
|
# delete the order
|
|
sub action_delete {
|
|
my ($self) = @_;
|
|
|
|
my $errors = $self->delete();
|
|
|
|
if (scalar @{ $errors }) {
|
|
$self->js->flash('error', $_) foreach @{ $errors };
|
|
return $self->js->render();
|
|
}
|
|
|
|
my $text = $self->type eq sales_order_intake_type() ? $::locale->text('The order intake has been deleted')
|
|
: $self->type eq sales_order_type() ? $::locale->text('The order confirmation has been deleted')
|
|
: $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
|
|
: $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
|
|
: $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
|
|
: $self->type eq purchase_quotation_intake_type() ? $::locale->text('The quotation intake has been deleted')
|
|
: '';
|
|
flash_later('info', $text);
|
|
|
|
my @redirect_params = (
|
|
action => 'add',
|
|
type => $self->type,
|
|
);
|
|
|
|
$self->redirect_to(@redirect_params);
|
|
}
|
|
|
|
# save the order
|
|
sub action_save {
|
|
my ($self) = @_;
|
|
|
|
my $errors = $self->save();
|
|
|
|
if (scalar @{ $errors }) {
|
|
$self->js->flash('error', $_) foreach @{ $errors };
|
|
return $self->js->render();
|
|
}
|
|
|
|
my $text = $self->type eq sales_order_intake_type() ? $::locale->text('The order intake has been saved')
|
|
: $self->type eq sales_order_type() ? $::locale->text('The order confirmation has been saved')
|
|
: $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
|
|
: $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
|
|
: $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
|
|
: $self->type eq purchase_quotation_intake_type() ? $::locale->text('The quotation intake has been saved')
|
|
: '';
|
|
flash_later('info', $text);
|
|
|
|
my @redirect_params;
|
|
if ($::form->{back_to_caller}) {
|
|
@redirect_params = $::form->{callback} ? ($::form->{callback})
|
|
: (controller => 'LoginScreen', action => 'user_login');
|
|
|
|
} else {
|
|
@redirect_params = (
|
|
action => 'edit',
|
|
type => $self->type,
|
|
id => $self->order->id,
|
|
callback => $::form->{callback},
|
|
);
|
|
}
|
|
|
|
$self->redirect_to(@redirect_params);
|
|
}
|
|
|
|
# create new version and set version number
|
|
sub action_add_subversion {
|
|
my ($self) = @_;
|
|
|
|
my $current_version_number = $self->order->current_version_number;
|
|
my $new_version_number = $current_version_number + 1;
|
|
|
|
my $new_number = $self->order->number;
|
|
$new_number =~ s/-$current_version_number$//;
|
|
$self->order->number($new_number . '-' . $new_version_number);
|
|
$self->order->add_order_version(SL::DB::OrderVersion->new(oe_id => $self->order->id,
|
|
version => $new_version_number));
|
|
|
|
# call the save action
|
|
$self->action_save();
|
|
|
|
}
|
|
|
|
# save the order as new document and open it for edit
|
|
sub action_save_as_new {
|
|
my ($self) = @_;
|
|
|
|
my $order = $self->order;
|
|
|
|
if (!$order->id) {
|
|
$self->js->flash('error', t8('This object has not been saved yet.'));
|
|
return $self->js->render();
|
|
}
|
|
|
|
# load order from db to check if values changed
|
|
my $saved_order = SL::DB::Order->new(id => $order->id)->load;
|
|
|
|
my %new_attrs;
|
|
# Lets assign a new number if the user hasn't changed the previous one.
|
|
# If it has been changed manually then use it as-is.
|
|
$new_attrs{number} = (trim($order->number) eq $saved_order->number)
|
|
? ''
|
|
: trim($order->number);
|
|
|
|
# Clear transdate unless changed
|
|
$new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
|
|
? DateTime->today_local
|
|
: $order->transdate;
|
|
|
|
# Set new reqdate unless changed if it is enabled in client config
|
|
if ($order->reqdate == $saved_order->reqdate) {
|
|
my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval :
|
|
$self->type eq sales_order_type() ? $::instance_conf->get_delivery_date_interval :
|
|
$self->type eq sales_order_intake_type() ? $::instance_conf->get_delivery_date_interval : 1;
|
|
|
|
if ( ($self->type eq sales_order_intake_type() && !$::instance_conf->get_deliverydate_on)
|
|
|| ($self->type eq sales_order_type() && !$::instance_conf->get_deliverydate_on)
|
|
|| ($self->type eq sales_quotation_type() && !$::instance_conf->get_reqdate_on)) {
|
|
$new_attrs{reqdate} = '';
|
|
} else {
|
|
$new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
|
|
}
|
|
} else {
|
|
$new_attrs{reqdate} = $order->reqdate;
|
|
}
|
|
|
|
# Update employee
|
|
$new_attrs{employee} = SL::DB::Manager::Employee->current;
|
|
|
|
# Warn on obsolete items
|
|
my @obsolete_positions = map { $_->position } grep { $_->part->obsolete } @{ $order->items_sorted };
|
|
flash_later('warning', t8('This record containts obsolete items at position #1', join ', ', @obsolete_positions)) if @obsolete_positions;
|
|
|
|
# Create new record from current one
|
|
$self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
|
|
|
|
# no linked records on save as new
|
|
delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
|
|
|
|
if (!$::form->{form_validity_token}) {
|
|
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_ORDER_SAVE())->token;
|
|
}
|
|
|
|
# save
|
|
$self->action_save();
|
|
}
|
|
|
|
# print the order
|
|
#
|
|
# This is called if "print" is pressed in the print dialog.
|
|
# If PDF creation was requested and succeeded, the pdf is offered for download
|
|
# via send_file (which uses ajax in this case).
|
|
sub action_print {
|
|
my ($self) = @_;
|
|
|
|
my $errors = $self->save();
|
|
|
|
if (scalar @{ $errors }) {
|
|
$self->js->flash('error', $_) foreach @{ $errors };
|
|
return $self->js->render();
|
|
}
|
|
|
|
$self->js_reset_order_and_item_ids_after_save;
|
|
|
|
my $redirect_url = $self->url_for(
|
|
action => 'edit',
|
|
type => $self->type,
|
|
id => $self->order->id,
|
|
);
|
|
|
|
my $format = $::form->{print_options}->{format};
|
|
my $media = $::form->{print_options}->{media};
|
|
my $formname = $::form->{print_options}->{formname};
|
|
my $copies = $::form->{print_options}->{copies};
|
|
my $groupitems = $::form->{print_options}->{groupitems};
|
|
my $printer_id = $::form->{print_options}->{printer_id};
|
|
|
|
# only PDF, OpenDocument & HTML for now
|
|
if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf html)) {
|
|
flash_later('error', t8('Format \'#1\' is not supported yet/anymore.', $format));
|
|
return $self->js->redirect_to($redirect_url)->render;
|
|
}
|
|
|
|
# only screen or printer by now
|
|
if (none { $media eq
my ($self) = @_;
|
|
|
|
|
my $previousform = $::auth->save_form_in_session(non_scalars => 1);
|
|
|
|
my $callback = $self->url_for(
|
|
action => 'return_from_create_part',
|
|
type => $self->type, # type is needed for check_auth on return
|
|
previousform => $previousform,
|
|
);
|
|
|
|
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.'));
|
|
|
|
my @redirect_params = (
|
|
controller => ' |