From: Bernd Bleßmann Date: Mon, 7 Mar 2016 11:16:28 +0000 (+0100) Subject: Auftrags-Controller: neue Eingabemakse für Aufträge basierend auf Controller X-Git-Tag: release-3.4.1~355 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=099fc63b531b1d4495cfab535ffd296d6a62ca0a;p=kivitendo-erp.git Auftrags-Controller: neue Eingabemakse für Aufträge basierend auf Controller --- diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm new file mode 100644 index 000000000..759334155 --- /dev/null +++ b/SL/Controller/Order.pm @@ -0,0 +1,386 @@ +package SL::Controller::Order; + +use strict; +use parent qw(SL::Controller::Base); + +use SL::Helper::Flash; +use SL::Presenter; +use SL::PriceSource; + +use SL::DB::Order; +use SL::DB::Customer; +use SL::DB::Vendor; +use SL::DB::TaxZone; +use SL::DB::Employee; +use SL::DB::Project; +use SL::DB::Default; +use SL::DB::Unit; + +use SL::Helper::DateTime; + +use List::Util qw(max first); +use List::MoreUtils qw(none pairwise); + +use Rose::Object::MakeMethods::Generic +( + 'scalar --get_set_init' => [ qw(order valid_types type cv p) ], +); + + +# safety +__PACKAGE__->run_before('_check_auth'); + +__PACKAGE__->run_before('_recalc', + only => [ qw(edit update save) ]); + +__PACKAGE__->run_before('_get_unalterable_data', + only => [ qw(save save_and_delivery_order create_pdf send_email) ]); + +# +# actions +# + +sub action_add { + my ($self) = @_; + + $self->order->transdate(DateTime->now_local()); + + $self->_pre_render(); + $self->render( + 'order/form', + title => $self->type eq _sales_order_type() ? $::locale->text('Add Sales Order') + : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order') + : '', + %{$self->{template_args}} + ); +} + +sub action_edit { + my ($self) = @_; + + $self->_pre_render(); + $self->render( + 'order/form', + title => $self->type eq _sales_order_type() ? $::locale->text('Edit Sales Order') + : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order') + : '', + %{$self->{template_args}} + ); +} + +sub action_update { + my ($self) = @_; + + $self->_pre_render(); + $self->render( + 'order/form', + title => $self->type eq _sales_order_type() ? $::locale->text('Edit Sales Order') + : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order') + : '', + %{$self->{template_args}} + ); +} + +sub action_save { + my ($self) = @_; + + my $errors = $self->_save(); + + if (scalar @{ $errors }) { + $self->js->flash('error', $_) foreach @{ $errors }; + return $self->js->render(); + } + + flash_later('info', $::locale->text('The order has been saved')); + my @redirect_params = ( + action => 'edit', + type => $self->type, + id => $self->order->id, + ); + + $self->redirect_to(@redirect_params); +} + +sub action_customer_vendor_changed { + my ($self) = @_; + + my $cv_method = $self->cv; + + if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) { + $self->js->show('#cp_row'); + } else { + $self->js->hide('#cp_row'); + } + + if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) { + $self->js->show('#shipto_row'); + } else { + $self->js->hide('#shipto_row'); + } + + $self->js + ->replaceWith('#order_cp_id', $self->build_contact_select) + ->replaceWith('#order_shipto_id', $self->build_shipto_select) + ->val('#order_taxzone_id', $self->order->taxzone_id) + ->focus('#order_' . $self->cv . '_id') + ->render($self); +} + +sub action_add_item { + my ($self) = @_; + + my $form_attr = $::form->{add_item}; + + return unless $form_attr->{parts_id}; + + my $item = SL::DB::OrderItem->new; + $item->assign_attributes(%$form_attr); + + my $part = SL::DB::Part->new(id => $form_attr->{parts_id})->load; + my $cv_method = $self->cv; + my $cv_discount = $self->order->$cv_method? $self->order->$cv_method->discount : 0.0; + + my %new_attr; + $new_attr{part} = $part; + $new_attr{description} = $part->description if ! $item->description; + $new_attr{qty} = 1.0 if ! $item->qty; + $new_attr{unit} = $part->unit; + $new_attr{sellprice} = $part->sellprice if ! $item->sellprice; + $new_attr{discount} = $cv_discount if ! $item->discount; + + # add_custom_variables adds cvars to an orderitem with no cvars for saving, but + # they cannot be retrieved via custom_variables until the order/orderitem is + # saved. Adding empty custom_variables to new orderitem here solves this problem. + $new_attr{custom_variables} = []; + + $item->assign_attributes(%new_attr); + + $self->order->add_items($item); + + $self->_recalc(); + + my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000); + my $row_as_html = $self->p->render('order/tabs/_row', ITEM => $item, ID => $item_id); + + $self->js + ->append('#row_table_id', $row_as_html) + ->val('.add_item_input', '') + ->run('row_table_scroll_down') + ->run('row_set_keyboard_events_by_id', $item_id) + ->on('.recalc', 'change', 'recalc_amounts_and_taxes') + ->focus('#add_item_parts_id_name'); + + $self->_js_redisplay_amounts_and_taxes; + $self->js->render(); +} + +sub action_recalc_amounts_and_taxes { + my ($self) = @_; + + $self->_recalc(); + + $self->_js_redisplay_linetotals; + $self->_js_redisplay_amounts_and_taxes; + $self->js->render(); +} + +sub _js_redisplay_linetotals { + my ($self) = @_; + + my @data = map {$::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0)} @{ $self->order->items }; + $self->js + ->run('redisplay_linetotals', \@data); +} + +sub _js_redisplay_amounts_and_taxes { + my ($self) = @_; + + $self->js + ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2)) + ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2)) + ->remove('.tax_row') + ->insertBefore($self->build_tax_rows, '#amount_row_id'); +} + +# +# helpers +# + +sub init_valid_types { + [ _sales_order_type(), _purchase_order_type() ]; +} + +sub init_type { + my ($self) = @_; + + if (none { $::form->{type} eq $_ } @{$self->valid_types}) { + die "Not a valid type for order"; + } + + $self->type($::form->{type}); +} + +sub init_cv { + my ($self) = @_; + + my $cv = $self->type eq _sales_order_type() ? 'customer' + : $self->type eq _purchase_order_type() ? 'vendor' + : die "Not a valid type for order"; + + return $cv; +} + +sub init_p { + SL::Presenter->get; +} + +sub init_order { + _make_order(); +} + +sub _check_auth { + my ($self) = @_; + + my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} }; + + my $right = $right_for->{ $self->type }; + $right ||= 'DOES_NOT_EXIST'; + + $::auth->assert($right); +} + +sub build_contact_select { + my ($self) = @_; + + $self->p->select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ], + value_key => 'cp_id', + title_key => 'full_name_dep', + default => $self->order->cp_id, + with_empty => 1, + style => 'width: 300px', + ); +} + +sub build_shipto_select { + my ($self) = @_; + + $self->p->select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ], + value_key => 'shipto_id', + title_key => 'displayable_id', + default => $self->order->shipto_id, + with_empty => 1, + style => 'width: 300px', + ); +} + +sub build_tax_rows { + my ($self) = @_; + + my $rows_as_html; + foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) { + $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax); + } + return $rows_as_html; +} + + +sub _make_order { + my ($self) = @_; + + # add_items adds items to an order with no items for saving, but they cannot + # be retrieved via items until the order is saved. Adding empty items to new + # order here solves this problem. + my $order; + $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id}; + $order ||= SL::DB::Order->new(orderitems => []); + + $order->assign_attributes(%{$::form->{order}}); + + return $order; +} + + +sub _recalc { + my ($self) = @_; + + # bb: todo: currency later + $self->order->currency_id($::instance_conf->get_currency_id()); + + my %pat = $self->order->calculate_prices_and_taxes(); + $self->{taxes} = []; + foreach my $tax_chart_id (keys %{ $pat{taxes} }) { + my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id); + push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id}, + tax => $tax }); + } + + pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}}; +} + + +sub _get_unalterable_data { + my ($self) = @_; + + foreach my $item (@{ $self->order->items }) { + if ($item->id) { + # load data from orderitems (db) + my $db_item = SL::DB::OrderItem->new(id => $item->id)->load; + $item->$_($db_item->$_) for qw(active_discount_source active_price_source longdescription); + } else { + # set data from part (or other sources) + $item->longdescription($item->part->notes); + #$item->active_price_source(''); + #$item->active_discount_source(''); + } + + # autovivify all cvars that are not in the form (cvars_by_config can do it). + # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values). + foreach my $var (@{ $item->cvars_by_config }) { + $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value})); + } + $item->parse_custom_variable_values; + } +} + + +sub _save { + my ($self) = @_; + + my $errors = []; + my $db = $self->order->db; + + $db->do_transaction( + sub { + $self->order->save(); + }) || push(@{$errors}, $db->error); + + return $errors; +} + + +sub _pre_render { + my ($self) = @_; + + $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted(); + $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id, + deleted => 0 ] ], + sort_by => 'name'); + $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id, + deleted => 0 ] ], + sort_by => 'name'); + $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id, + active => 1 ] ], + sort_by => 'projectnumber'); + + $self->{current_employee_id} = SL::DB::Manager::Employee->current->id; +} + +sub _sales_order_type { + 'sales_order'; +} + +sub _purchase_order_type { + 'purchase_order'; +} + +1; diff --git a/js/locale/de.js b/js/locale/de.js index 4aa60e136..5bbfceeff 100644 --- a/js/locale/de.js +++ b/js/locale/de.js @@ -52,6 +52,8 @@ namespace("kivi").setupLocale({ "Part picker":"Artikelauswahl", "Paste":"Einfügen", "Paste template":"Vorlage einfügen", +"Please select a customer.":"Bitte wählen Sie einen Kunden aus.", +"Please select a vendor.":"Bitte wählen Sie einen Lieferanten aus.", "Price Types":"Preistypen", "Project link actions":"Projektverknüpfungs-Aktionen", "Quotations/Orders actions":"Aktionen für Angebote/Aufträge", diff --git a/locale/de/all b/locale/de/all index 45f8625b5..219ad76ac 100755 --- a/locale/de/all +++ b/locale/de/all @@ -2019,8 +2019,10 @@ $self->{texts} = { 'Please re-run the analysis for broken general ledger entries by clicking this button:' => 'Bitte wiederholen Sie die Analyse der Hauptbucheinträge, indem Sie auf diesen Button klicken:', 'Please read the file' => 'Bitte lesen Sie die Datei', 'Please select a customer from the list below.' => 'Bitte einen Endkunden aus der Liste auswählen', + 'Please select a customer.' => 'Bitte wählen Sie einen Kunden aus.', 'Please select a part from the list below.' => 'Bitte wählen Sie einen Artikel aus der Liste aus.', 'Please select a vendor from the list below.' => 'Bitte einen Händler aus der Liste auswählen', + 'Please select a vendor.' => 'Bitte wählen Sie einen Lieferanten aus.', 'Please select the dataset you want to delete:' => 'Bitte wählen Sie die zu löschende Datenbank aus:', 'Please select the destination bank account for the collections:' => 'Bitte wählen Sie das Bankkonto als Ziel für die Einzüge aus:', 'Please select the source bank account for the transfers:' => 'Bitte wählen Sie das Bankkonto als Quelle für die Überweisungen aus:', @@ -2837,6 +2839,7 @@ $self->{texts} = { 'The number of days for full payment' => 'Die Anzahl Tage, bis die Rechnung in voller Höhe bezahlt werden muss', 'The numbering will start at 1 with each requirement spec.' => 'Die Nummerierung beginnt bei jedem Pflichtenheft bei 1.', 'The option field is empty.' => 'Das Optionsfeld ist leer.', + 'The order has been saved' => 'Der Auftrag wurde gespeichert.', 'The package name is invalid.' => 'Der Paketname ist ungültig.', 'The parts for this delivery order have already been transferred in.' => 'Die Artikel dieses Lieferscheins wurden bereits eingelagert.', 'The parts for this delivery order have already been transferred out.' => 'Die Artikel dieses Lieferscheins wurden bereits ausgelagert.', @@ -3390,6 +3393,7 @@ $self->{texts} = { 'dated' => 'datiert', 'debug' => 'Debug', 'delete' => 'Löschen', + 'delete item' => 'Position löschen', 'delivered' => 'geliefert', 'deliverydate' => 'Lieferdatum', 'difference as skonto' => 'Differenz als Skonto', diff --git a/templates/webpages/order/form.html b/templates/webpages/order/form.html new file mode 100644 index 000000000..62225e2eb --- /dev/null +++ b/templates/webpages/order/form.html @@ -0,0 +1,59 @@ +[%- USE T8 %] +[%- USE LxERP %] +[%- USE L %] + +
+
[% FORM.title %]
+ + [% L.hidden_tag('callback', FORM.callback) %] + [% L.hidden_tag('type', FORM.type) %] + [% L.hidden_tag('id', SELF.order.id) %] + + [%- INCLUDE 'common/flash.html' %] + +
+ + + [% PROCESS "order/tabs/basic_data.html" %] + [% PROCESS 'webdav/_list.html' %] +
+ [%- LxERP.t8("Loading...") %] +
+
+ +
+ + [% L.hidden_tag('action', 'Order/dispatch') %] + + [% L.button_tag('save()', LxERP.t8('Save')) %] + +
+ + + diff --git a/templates/webpages/order/tabs/_item_input.html b/templates/webpages/order/tabs/_item_input.html new file mode 100644 index 000000000..f59c22628 --- /dev/null +++ b/templates/webpages/order/tabs/_item_input.html @@ -0,0 +1,26 @@ +[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %] + +
+ + + + + + + + + + + + + + + + + + + + + +
[%- 'Part' | $T8 %] [%- 'Description' | $T8 %] [%- 'Qty' | $T8 %] [%- 'Price' | $T8 %] [%- 'Discount' | $T8 %]
[% L.part_picker('add_item.parts_id', '', fat_set_item=1, style='width: 300px', class="add_item_input") %][% L.input_tag('add_item.description', '', class="add_item_input") %][% L.input_tag('add_item.qty_as_number', '', size = 5, style='text-align:right', class="add_item_input") %][% L.input_tag('add_item.sellprice_as_number', '', size = 10, style='text-align:right', class="add_item_input") %][% L.input_tag('add_item.discount_as_percent', '', size = 5, style='text-align:right', class="add_item_input") %][% L.button_tag('add_item()', LxERP.t8('Add part')) %]
+
diff --git a/templates/webpages/order/tabs/_row.html b/templates/webpages/order/tabs/_row.html new file mode 100644 index 000000000..fd12344f6 --- /dev/null +++ b/templates/webpages/order/tabs/_row.html @@ -0,0 +1,95 @@ +[%- USE T8 %] +[%- USE HTML %] +[%- USE LxERP %] +[%- USE L %] + + + + + + [% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %] + [% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %] + + + [%- LxERP.t8('reorder item') %] + + + [%- L.button_tag("delete_order_item_row(this)", + LxERP.t8("X"), + confirm=LxERP.t8("Are you sure?")) %] + + + [% HTML.escape(ITEM.part.partnumber) %] + + + [% L.input_tag("order.orderitems[].description", + ITEM.description, + style='width: 300px') %] + + + [%- L.input_tag("order.orderitems[].qty_as_number", + ITEM.qty_as_number, + size = 5, + style='text-align:right', + class="recalc") %] + + + [%- L.input_tag("order.orderitems[].price_factor", + ITEM.price_factor, + size = 5, + style='text-align:right', + class="recalc") %] + + + [%- L.input_tag("order.orderitems[].unit", + ITEM.unit, + size = 5, + class="recalc") %] + + + [%- L.input_tag("order.orderitems[].sellprice_as_number", + ITEM.sellprice_as_number, + size = 10, + style='text-align:right', + class="recalc") %] + + + [%- L.input_tag("order.orderitems[].discount_as_percent", + ITEM.discount_as_percent, + size = 5, + style='text-align:right', + class="recalc") %] + + + [%- L.div_tag(LxERP.format_amount(ITEM.linetotal, 2, 0), name="linetotal") %] + + + + + + + + + [%- SET n = 0 %] + [%- FOREACH var = ITEM.cvars_by_config %] + [%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %] + [%- SET n = n + 1 %] + + + [%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %] + + [%- END %] + [%- END %] + +
+ [% var.config.description %] + + [% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %] + [% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %] + [% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %] + [% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %] +
+ + + + diff --git a/templates/webpages/order/tabs/_tax_row.html b/templates/webpages/order/tabs/_tax_row.html new file mode 100644 index 000000000..010da3fba --- /dev/null +++ b/templates/webpages/order/tabs/_tax_row.html @@ -0,0 +1,9 @@ +[%- USE T8 %] +[%- USE HTML %] +[%- USE LxERP %] +[%- USE L %] + + + [%- TAX.tax.taxdescription %] [% TAX.tax.rate_as_percent %]% + [%- LxERP.format_amount(TAX.amount, 2, 0) %] + diff --git a/templates/webpages/order/tabs/basic_data.html b/templates/webpages/order/tabs/basic_data.html new file mode 100644 index 000000000..0cd668c3e --- /dev/null +++ b/templates/webpages/order/tabs/basic_data.html @@ -0,0 +1,296 @@ +[%- USE T8 %] +[%- USE HTML %] +[%- USE LxERP %] +[%- USE L %] + +
+ + + + + + +
+ + + + [% SET cv_id = SELF.cv _ '_id' %] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
[% SELF.cv | $T8 %][% L.customer_vendor_picker("order.${SELF.cv}" _ '_id', SELF.order.$cv_id, type=SELF.cv, style='width: 300px') %]
[% 'Contact Person' | $T8 %][% L.select_tag('order.cp_id', + SELF.order.${SELF.cv}.contacts, + default=SELF.order.cp_id, + title_key='full_name_dep', + value_key='cp_id', + with_empty=1, + style='width: 300px') %]
[% 'Shipping Address' | $T8 %][% L.select_tag('order.shipto_id', + SELF.order.${SELF.cv}.shipto, + default=SELF.order.shipto_id, + title_key='displayable_id', + value_key='shipto_id', + with_empty=1, + style='width: 300px') %]
[% 'Steuersatz' | $T8 %][% L.select_tag('order.taxzone_id', SELF.all_taxzones, default=SELF.order.taxzone_id, title_key='description', style='width: 300px') %]
[% 'Shipping Point' | $T8 %][% L.input_tag('order.shippingpoint', SELF.order.shippingpoint, style='width: 300px') %]
[% 'Ship via' | $T8 %][% L.input_tag('order.shipvia', SELF.order.shipvia, style='width: 300px') %]
[% 'Transaction description' | $T8 %][% L.input_tag('order.transaction_description', SELF.order.transaction_description, style='width: 300px') %]
+
+ + + + + + + + + + + + [% IF SELF.cv == 'customer' %] + + + + + [% END %] + + + + + + + + + + + + + + + + + + + + + +
+ [%- IF SELF.order.id %] + + [% L.yes_no_tag('order.delivered', SELF.order.delivered) %] + + [% L.yes_no_tag('order.closed', SELF.order.closed) %] + [%- END %] +
[% 'Employee' | $T8 %][% L.select_tag('order.employee_id', + SELF.all_employees, + default=(SELF.order.employee_id ? SELF.order.employee_id : SELF.current_employee_id), + title_key='safe_name') %]
[% 'Salesman' | $T8 %][% L.select_tag('order.salesman_id', + SELF.all_salesmen, + default=(SELF.order.salesman_id ? SELF.order.salesman_id : SELF.current_employee_id), + title_key='safe_name') %]
[% 'Order Number' | $T8 %][% L.input_tag('order.ordnumber', SELF.order.ordnumber, size = 11) %]
[% 'Customer Order Number' | $T8 %][% L.input_tag('order.cusordnumber', SELF.order.cusordnumber, size = 11) %]
[% 'Order Date' | $T8 %][% L.date_tag('order.transdate', SELF.order.transdate) %]
[% 'Project Number' | $T8 %][%- L.select_tag('order.globalproject_id', SELF.all_projects, default=SELF.order.globalproject_id, title_key='projectnumber', with_empty = 1) %]
+ +
+ + [%- PROCESS order/tabs/_item_input.html %] + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + [%- FOREACH item = SELF.order.items_sorted %] + [%- PROCESS order/tabs/_row.html ITEM=item %] + [%- END %] + +
[%- LxERP.t8('reorder item') %][%- LxERP.t8('delete item') %][%- 'Partnumber' | $T8 %] [%- 'Description' | $T8 %] [%- 'Qty' | $T8 %] [%- 'Price Factor' | $T8 %] [%- 'Unit' | $T8 %] [%- 'Price' | $T8 %] [%- 'Discount' | $T8 %] [%- 'Extended' | $T8 %]
+
+ +
+ + [%- IF NOT taxincluded %] + + + + + [%- END %] + [%- FOREACH tax = SELF.taxes %] + [%- PROCESS order/tabs/_tax_row.html TAX=tax %] + [%- END %] + + + + + +
[%- 'Subtotal' | $T8 %] + [%- L.div_tag(SELF.order.netamount_as_number, id='netamount_id') %] +
[%- 'Total' | $T8 %] + + [%- IF NOT taxincluded %] + + + + + [%- END %] + [%- FOREACH tax = SELF.taxes %] + [%- PROCESS order/tabs/_tax_row.html TAX=tax %] + [%- END %] + + + + +
[%- 'Subtotal' | $T8 %] + [%- L.div_tag(SELF.order.netamount_as_number, id='netamount_id') %] +
[%- 'Total' | $T8 %] + [%- L.div_tag(SELF.order.amount_as_number, id='amount_id') %] +
+
+
+ +
+ + +[% L.sortable_element('#row_table_id') %] + +