]> wagnertech.de Git - mfinanz.git/commitdiff
Merge branch 'master' of github.com:kivitendo/kivitendo-erp
authorJan Büren <jan@kivitendo-premium.de>
Thu, 28 Aug 2014 12:18:33 +0000 (14:18 +0200)
committerJan Büren <jan@kivitendo-premium.de>
Thu, 28 Aug 2014 12:18:33 +0000 (14:18 +0200)
41 files changed:
SL/AP.pm
SL/AR.pm
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/Controller/FinancialControllingReport.pm
SL/Controller/FinancialOverview.pm
SL/Controller/RequirementSpec.pm
SL/Controller/RequirementSpecOrder.pm
SL/Controller/RequirementSpecPart.pm [new file with mode: 0644]
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.pm
SL/DB/Manager/RequirementSpec.pm
SL/DB/Manager/RequirementSpecPart.pm [new file with mode: 0644]
SL/DB/Manager/Unit.pm
SL/DB/MetaSetup/DeliveryOrder.pm
SL/DB/MetaSetup/Invoice.pm
SL/DB/MetaSetup/Order.pm
SL/DB/MetaSetup/PurchaseInvoice.pm
SL/DB/MetaSetup/RequirementSpecPart.pm [new file with mode: 0644]
SL/DB/PeriodicInvoicesConfig.pm
SL/DB/RequirementSpec.pm
SL/DB/RequirementSpecPart.pm [new file with mode: 0644]
SL/DB/Unit.pm
SL/DO.pm
SL/Form.pm
SL/IR.pm
SL/IS.pm
SL/OE.pm
js/locale/de.js
js/requirement_spec.js
locale/de/all
sql/Pg-upgrade2/requirement_spec_parts.sql [new file with mode: 0644]
sql/Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql [new file with mode: 0644]
templates/webpages/common/show_vc_details.html
templates/webpages/requirement_spec/select_template_to_paste.html
templates/webpages/requirement_spec/show.html
templates/webpages/requirement_spec_order/_assignment_form.html
templates/webpages/requirement_spec_order/list.html
templates/webpages/requirement_spec_order/update.html
templates/webpages/requirement_spec_part/_edit.html [new file with mode: 0644]
templates/webpages/requirement_spec_part/_part.html [new file with mode: 0644]
templates/webpages/requirement_spec_part/show.html [new file with mode: 0644]

index cc7e06f94bacb4cd0700968f1f3baf3e6d8117fd..4d651e4124b9674b23dc383ef7f83138867772f6 100644 (file)
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -167,10 +167,10 @@ sub post_transaction {
       ($form->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('glid')|);
  
       $query =
-        qq|INSERT INTO ap (id, invnumber, employee_id,currency_id) | .
+        qq|INSERT INTO ap (id, invnumber, employee_id,currency_id, taxzone_id) | .
         qq|VALUES (?, ?, (SELECT e.id FROM employee e WHERE e.login = ?),
-                      (SELECT id FROM currencies WHERE name = ?) )|;
-      do_query($form, $dbh, $query, $form->{id}, $form->{invnumber}, $form->{login}, $form->{currency});
+                      (SELECT id FROM currencies WHERE name = ?), (SELECT taxzone_id FROM vendor WHERE id = ?) )|;
+      do_query($form, $dbh, $query, $form->{id}, $form->{invnumber}, $form->{login}, $form->{currency}, $form->{vendor_id});
 
     }
 
index a842b9da5a99e2c1a20765f9b87cb89d0f36444f..95aba2680e906805620991afa94d773f2a172586 100644 (file)
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -135,8 +135,8 @@ sub post_transaction {
     } else {
       $query = qq|SELECT nextval('glid')|;
       ($form->{id}) = selectrow_query($form, $dbh, $query);
-      $query = qq|INSERT INTO ar (id, invnumber, employee_id, currency_id) VALUES (?, 'dummy', ?, (SELECT id FROM currencies WHERE name=?))|;
-      do_query($form, $dbh, $query, $form->{id}, $form->{employee_id}, $form->{currency});
+      $query = qq|INSERT INTO ar (id, invnumber, employee_id, currency_id, taxzone_id) VALUES (?, 'dummy', ?, (SELECT id FROM currencies WHERE name=?), (SELECT taxzone_id FROM customer WHERE id = ?))|;
+      do_query($form, $dbh, $query, $form->{id}, $form->{employee_id}, $form->{currency}, $form->{customer_id});
       if (!$form->{invnumber}) {
         my $trans_number   = SL::TransNumber->new(type => 'invoice', dbh => $dbh, number => $form->{partnumber}, id => $form->{id});
         $form->{invnumber} = $trans_number->create_unique;
index 3b19811df25165123edc46a1cb52e6194b6e2f65..91e8efff90e3eb001ba1c10c4a7e9036b58ac72c 100644 (file)
@@ -98,7 +98,7 @@ sub _generate_time_period_variables {
     next_year           => [ $period_start_date->clone->truncate(to => 'year')->add(     years => 1),   sub { $_[0]->year } ],
 
     period_start_date   => [ $period_start_date->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
-    period_end_date     => [ $period_end_date  ->clone->truncate(to => 'month'), sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
+    period_end_date     => [ $period_end_date,                                   sub { $::locale->format_date(\%::myconfig, $_[0]) } ],
   };
 
   return $vars;
index 4ffaabd0994534c1f4dff7b9b45fefa061978c45..4d0805446c7d1b0938e858cde2b5ca9acde3ead4 100644 (file)
@@ -120,7 +120,7 @@ sub calculate_data {
     $order->{billable_amount}   = $order->{delivered_amount} - $order->{billed_amount};
 
     if ($order->periodic_invoices_config) {
-      my @dates = $order->periodic_invoices_config->calculate_invoice_dates(past_dates => 1, end_date => $order->periodic_invoices_config->end_date || DateTime->today_local);
+      my @dates = $order->periodic_invoices_config->calculate_invoice_dates(past_dates => 1, end_date => DateTime->today_local);
       $order->{net_amount} = $order->netamount * scalar(@dates);
 
     } else {
index 49c04698ce6638a9eb304a38d3f913cb095f0842..e8ed1401a7eece88acedbed4f1e4ba4564588f42 100644 (file)
@@ -91,7 +91,7 @@ sub get_objects {
     purchase_orders        => SL::DB::Manager::Order->get_all(          where => [ and => [ @f_date, @f_salesman, SL::DB::Manager::Order->type_filter('purchase_order')    ]]),
     sales_invoices         => SL::DB::Manager::Invoice->get_all(        where => [ and => [ @f_date, @f_salesman, ]]),
     purchase_invoices      => SL::DB::Manager::PurchaseInvoice->get_all(where => [ and =>  \@f_date ]),
-    periodic_invoices_cfg  => SL::DB::Manager::PeriodicInvoicesConfig->get_all(where => [ active => 1 ]),
+    periodic_invoices_cfg  => SL::DB::Manager::PeriodicInvoicesConfig->get_all(where => [ active => 1, $self->salesman_id ? ('order.salesman_id' => $self->salesman_id) : () ], with_objects => [ qw(order) ]),
   });
 
   $self->objects->{sales_orders} = [ grep { !$_->periodic_invoices_config || !$_->periodic_invoices_config->active } @{ $self->objects->{sales_orders} } ];
index 7e25d7e7ded7c73c58b6c02ca04010e177627157..f762b9794a14a4fdd19d25033757d5bb62306f32 100644 (file)
@@ -29,7 +29,7 @@ use Rose::Object::MakeMethods::Generic
 (
   scalar                  => [ qw(requirement_spec_item visible_item visible_section) ],
   'scalar --get_set_init' => [ qw(requirement_spec customers types statuses complexities risks projects project_types project_statuses default_project_type default_project_status copy_source js
-                                  current_text_block_output_position models) ],
+                                  current_text_block_output_position models time_based_units) ],
 );
 
 __PACKAGE__->run_before('setup');
@@ -234,7 +234,10 @@ sub action_create_pdf {
 sub action_select_template_to_paste {
   my ($self) = @_;
 
-  my @templates = grep { @{ $_->sections } || @{ $_->text_blocks } } @{ SL::DB::Manager::RequirementSpec->get_all(where => [ is_template => 1 ], sort_by => 'lower(title)') };
+  my @templates = @{ SL::DB::Manager::RequirementSpec->get_all(
+    where   => [ is_template => 1, SL::DB::Manager::RequirementSpec->not_empty_filter ],
+    sort_by => 'lower(requirement_specs.title)',
+  ) };
   $self->render('requirement_spec/select_template_to_paste', { layout => 0 }, TEMPLATES => \@templates);
 }
 
@@ -253,6 +256,12 @@ sub action_paste_template {
     $self->render_first_pasted_section_as_list($result{sections}->[0]);
   }
 
+  my $parts_list = $self->render('requirement_spec_part/show', { output => 0 });
+  $self->js
+    ->replaceWith('#additional_parts_list_container', $parts_list)
+    ->show(       '#additional_parts_list_container')
+    ->remove(     '#additional_parts_form_container');
+
   $self->invalidate_version->render($self);
 }
 
@@ -280,6 +289,7 @@ sub init_project_types          { SL::DB::Manager::ProjectType->get_all_sorted
 sub init_projects               { SL::DB::Manager::Project->get_all_sorted                    }
 sub init_risks                  { SL::DB::Manager::RequirementSpecRisk->get_all_sorted        }
 sub init_statuses               { SL::DB::Manager::RequirementSpecStatus->get_all_sorted      }
+sub init_time_based_units       { SL::DB::Manager::Unit->time_based_units                     }
 sub init_types                  { SL::DB::Manager::RequirementSpecType->get_all_sorted        }
 
 sub init_customers {
index 8c33d6ffdbf9a8f4d681b6b04e0032f81d1fdda3..352cf636c7a9cd3616f33833d600b065fd09b1c1 100644 (file)
@@ -23,7 +23,7 @@ use constant FORMS_SELECTOR => '#quotations_and_orders_article_assignment,#quota
 use Rose::Object::MakeMethods::Generic
 (
   scalar                  => [ qw(parts) ],
-  'scalar --get_set_init' => [ qw(requirement_spec rs_order js h_unit_name all_customers all_parts_time_unit) ],
+  'scalar --get_set_init' => [ qw(requirement_spec rs_order js h_unit_name all_customers all_parts_time_unit section_order_part) ],
 );
 
 __PACKAGE__->run_before('setup');
@@ -66,14 +66,11 @@ sub action_create {
   $sections_by_id{ $_->{id} }->update_attributes(order_part_id => $_->{order_part_id}) for @{ $section_attrs };
 
   # 2. Create actual quotation/order.
-  my $order = $self->create_order(sections => $sections);
+  my $order = $self->create_order(sections => $sections, additional_parts => $self->requirement_spec->parts_sorted);
   $order->db->with_transaction(sub {
     $order->save;
 
-    $self->requirement_spec->orders(
-      @{ $self->requirement_spec->orders },
-      SL::DB::RequirementSpecOrder->new(order => $order, version => $self->requirement_spec->version)
-    );
+    $self->requirement_spec->add_orders(SL::DB::RequirementSpecOrder->new(order => $order, version => $self->requirement_spec->version));
     $self->requirement_spec->save;
 
     $self->requirement_spec->link_to_record($order);
@@ -123,31 +120,13 @@ sub action_update {
 }
 
 sub action_do_update {
-  my ($self)           = @_;
-
-  my $order            = $self->rs_order->order;
-  my $sections         = $self->requirement_spec->sections_sorted;
-  my %orderitems_by_id = map { ($_->id => $_) } @{ $order->orderitems };
-  my %sections_by_id   = map { ($_->id => $_) } @{ $sections };
-  $self->{parts}       = { map { ($_->id => $_) } @{ SL::DB::Manager::Part->get_all(where => [ id => [ uniq map { $_->order_part_id } @{ $sections } ] ]) } };
-  my $language_id      = $self->requirement_spec->customer->language_id;
-
-  my %sections_seen;
-
-  foreach my $attributes (@{ $::form->{orderitems} || [] }) {
-    my $orderitem = $orderitems_by_id{ $attributes->{id}         };
-    my $section   = $sections_by_id{   $attributes->{section_id} };
-    next unless $orderitem && $section;
-
-    $self->create_order_item(section => $section, item => $orderitem, language_id => $language_id)->save;
-    $sections_seen{ $section->id } = 1;
-  }
+  my ($self)         = @_;
 
-  my @new_orderitems = map  { $self->create_order_item(section => $_, language_id => $language_id) }
-                       grep { !$sections_seen{ $_->id } }
-                       @{ $sections };
+  my $order          = $self->rs_order->order;
+  my @new_orderitems =  $self->do_update_sections;
+  push @new_orderitems, $self->do_update_additional_parts;
 
-  $order->orderitems([ @{ $order->orderitems }, @new_orderitems ]) if @new_orderitems;
+  $order->add_orderitems(\@new_orderitems) if @new_orderitems;
 
   $order->calculate_prices_and_taxes;
 
@@ -177,6 +156,7 @@ sub action_edit_assignment {
   my $html   = $self->render('requirement_spec_order/edit_assignment', { output => 0 }, make_part_title => sub { $_[0]->partnumber . ' ' . $_[0]->description });
   $self->js->hide(LIST_SELECTOR())
            ->after(LIST_SELECTOR(), $html)
+           ->reinit_widgets
            ->render($self);
 }
 
@@ -230,15 +210,16 @@ sub init_js {
 }
 
 sub init_all_customers { SL::DB::Manager::Customer->get_all_sorted }
-sub init_h_unit_name   { first { SL::DB::Manager::Unit->find_by(name => $_) } qw(Std h Stunde) };
+sub init_h_unit_name   { SL::DB::Manager::Unit->find_h_unit->name };
 sub init_rs_order      { SL::DB::RequirementSpecOrder->new(id => $::form->{rs_order_id})->load };
+sub init_section_order_part { my $id = $::instance_conf->get_requirement_spec_section_order_part_id; return $id ? SL::DB::Part->new(id => $id)->load : undef }
 
 sub init_all_parts_time_unit {
   my ($self) = @_;
 
   return [] unless $self->h_unit_name;
 
-  my @convertible_unit_names = map { $_->name } @{ SL::DB::Manager::Unit->find_by(name => $self->h_unit_name)->convertible_units };
+  my @convertible_unit_names = map { $_->name } @{ SL::DB::Manager::Unit->time_based_units };
 
   return SL::DB::Manager::Part->get_all_sorted(where => [ unit => \@convertible_unit_names ]);
 }
@@ -247,9 +228,72 @@ sub init_all_parts_time_unit {
 # helpers
 #
 
-sub load_parts_for_sections {
-  my ($self, %params) = @_;
+sub cache_parts {
+  my ($self, @ids) = @_;
+
+  my $parts = !@ids ? [] : SL::DB::Manager::Part->get_all(
+    where        => [ id => \@ids ],
+    with_objects => [ qw(unit_obj) ],
+  );
+
+  $self->parts({ map { ($_->id => $_) } @{ $parts } });
+}
+
+sub do_update_sections {
+  my ($self)           = @_;
+
+  my $order            = $self->rs_order->order;
+  my $sections         = $self->requirement_spec->sections_sorted;
+  my %orderitems_by_id = map { ($_->id => $_) } @{ $order->orderitems };
+  my %sections_by_id   = map { ($_->id => $_) } @{ $sections };
+  my $language_id      = $self->requirement_spec->customer->language_id;
+
+  $self->cache_parts(uniq map { $_->order_part_id } @{ $sections });
+
+  my %sections_seen;
+
+  foreach my $attributes (@{ $::form->{orderitems} || [] }) {
+    my $orderitem = $orderitems_by_id{ $attributes->{id}         };
+    my $section   = $sections_by_id{   $attributes->{section_id} };
+    next unless $orderitem && $section;
+
+    $self->create_order_item(section => $section, item => $orderitem, language_id => $language_id)->save;
+    $sections_seen{ $section->id } = 1;
+  }
+
+  my @new_orderitems = map  { $self->create_order_item(section => $_, language_id => $language_id) }
+                       grep { !$sections_seen{ $_->id } }
+                       @{ $sections };
+
+  return @new_orderitems;
+}
+
+sub do_update_additional_parts {
+  my ($self)        = @_;
+
+  my $order         = $self->rs_order->order;
+  my $add_parts     = $self->requirement_spec->parts_sorted;
+  my %orderitems_by = map { (($_->parts_id . '-' . $_->description) => $_) } @{ $order->items };
+  my $language_id   = $self->requirement_spec->customer->language_id;
+
+  $self->cache_parts(uniq map { $_->part_id } @{ $add_parts });
+
+  my %add_part_seen;
+  my @new_orderitems;
+
+  foreach my $add_part (@{ $add_parts }) {
+    my $key       = $add_part->part_id . '-' . $add_part->description;
+    my $orderitem = $orderitems_by{$key};
+
+    if ($orderitem) {
+      $self->create_additional_part_order_item(additional_part => $add_part, item => $orderitem, language_id => $language_id)->save;
+
+    } else {
+      push @new_orderitems, $self->create_additional_part_order_item(additional_part => $add_part, language_id => $language_id);
+    }
+  }
 
+  return @new_orderitems;
 }
 
 sub create_order_item {
@@ -258,6 +302,7 @@ sub create_order_item {
   my $section         = $params{section};
   my $item            = $params{item} || SL::DB::OrderItem->new;
   my $part            = $self->parts->{ $section->order_part_id };
+  my $is_time_based   = $part->unit_obj->is_time_based;
   my $translation     = $params{language_id} ? first { $params{language_id} == $_->language_id } @{ $part->translations } : {};
   my $description     = $section->{keep_description} ? $item->description : ($translation->{translation} || $part->description);
   my $longdescription = $translation->{longdescription} || $part->notes;
@@ -277,9 +322,34 @@ sub create_order_item {
     parts_id        => $part->id,
     description     => $description,
     longdescription => $longdescription,
-    qty             => $section->time_estimation * 1,
-    unit            => $self->h_unit_name,
-    sellprice       => $::form->round_amount($self->requirement_spec->hourly_rate, 2),
+    qty             => $is_time_based ? $section->time_estimation * 1 : 1,
+    unit            => $is_time_based ? $self->h_unit_name            : $part->unit,
+    sellprice       => $::form->round_amount($self->requirement_spec->hourly_rate * ($is_time_based ? 1 : $section->time_estimation), 2),
+    lastcost        => $part->lastcost,
+    discount        => 0,
+    project_id      => $self->requirement_spec->project_id,
+  );
+
+  return $item;
+}
+
+sub create_additional_part_order_item {
+  my ($self, %params) = @_;
+
+  my $add_part        = $params{additional_part};
+  my $item            = $params{item} || SL::DB::OrderItem->new;
+  my $part            = $self->parts->{ $add_part->part_id };
+  my $translation     = $params{language_id} ? first { $params{language_id} == $_->language_id } @{ $part->translations } : {};
+  my $description     = $item->description              || $add_part->description;
+  my $longdescription = $translation->{longdescription} || $part->notes;
+
+  $item->assign_attributes(
+    parts_id        => $part->id,
+    description     => $description,
+    longdescription => $longdescription,
+    qty             => $add_part->qty,
+    unit            => $add_part->unit->name,
+    sellprice       => $add_part->unit->convert_to($part->sellprice, $part->unit_obj),
     lastcost        => $part->lastcost,
     discount        => 0,
     project_id      => $self->requirement_spec->project_id,
@@ -291,17 +361,22 @@ sub create_order_item {
 sub create_order {
   my ($self, %params) = @_;
 
-  $self->{parts} = { map { ($_->{id} => $_) } @{ SL::DB::Manager::Part->get_all(where => [ id => [ uniq map { $_->{order_part_id} } @{ $params{sections} } ] ]) } };
+  my @part_ids = (
+    map({ $_->{order_part_id} } @{ $params{sections} }),
+    map({ $_->part_id         } @{ $params{additional_parts} }),
+  );
+  $self->{parts} = { map { ($_->{id} => $_) } @{ SL::DB::Manager::Part->get_all(where => [ id => [ uniq @part_ids ] ]) } };
 
   my $customer   = SL::DB::Customer->new(id => $::form->{customer_id})->load;
-  my @orderitems = map { $self->create_order_item(section => $_, language_id => $customer->language_id) } @{ $params{sections} };
+  my @orderitems = map { $self->create_order_item(                section => $_,         language_id => $customer->language_id) } @{ $params{sections} };
+  my @add_items  = map { $self->create_additional_part_order_item(additional_part => $_, language_id => $customer->language_id) } @{ $params{additional_parts} };
   my $employee   = SL::DB::Manager::Employee->current;
   my $order      = SL::DB::Order->new(
     globalproject_id        => $self->requirement_spec->project_id,
     transdate               => DateTime->today_local,
     reqdate                 => $::form->{quotation} && $customer->payment_id ? $customer->payment->calc_date : undef,
     quotation               => !!$::form->{quotation},
-    orderitems              => \@orderitems,
+    orderitems              => [ @orderitems, @add_items ],
     customer_id             => $customer->id,
     taxincluded             => $customer->taxincluded,
     intnotes                => $customer->notes,
diff --git a/SL/Controller/RequirementSpecPart.pm b/SL/Controller/RequirementSpecPart.pm
new file mode 100644 (file)
index 0000000..b9df7f5
--- /dev/null
@@ -0,0 +1,119 @@
+package SL::Controller::RequirementSpecPart;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use Carp;
+use List::MoreUtils qw(any);
+
+use SL::ClientJS;
+use SL::DB::Customer;
+use SL::DB::Project;
+use SL::DB::RequirementSpec;
+use SL::DB::RequirementSpecPart;
+use SL::Helper::Flash;
+use SL::Locale::String;
+
+use Rose::Object::MakeMethods::Generic
+(
+  'scalar --get_set_init' => [ qw(requirement_spec js) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+
+#
+# actions
+#
+
+sub action_show {
+  my ($self, %params) = @_;
+
+  $self->render('requirement_spec_part/show', { layout => 0 });
+}
+
+sub action_ajax_edit {
+  my ($self, %params) = @_;
+
+  my $html = $self->render('requirement_spec_part/_edit', { output => 0 });
+
+  $self->js
+   ->hide('#additional_parts_list_container')
+   ->after('#additional_parts_list_container', $html)
+   ->on('#edit_additional_parts_form INPUT[type=text]', 'keydown', 'kivi.requirement_spec.additional_parts_input_key_down')
+   ->focus('#additional_parts_add_part_id_name')
+   ->run('kivi.requirement_spec.prepare_edit_additional_parts_form')
+   ->reinit_widgets
+   ->render;
+}
+
+sub action_ajax_add {
+  my ($self)  = @_;
+
+  my $part      = SL::DB::Part->new(id => $::form->{part_id})->load(with_objects => [ qw(unit_obj) ]);
+  my $rs_part   = SL::DB::RequirementSpecPart->new(
+    part        => $part,
+    qty         => 1,
+    unit        => $part->unit_obj,
+    description => $part->description,
+  );
+  my $row       = $self->render('requirement_spec_part/_part', { output => 0 }, part => $rs_part);
+
+  $self->js
+   ->val(  '#additional_parts_add_part_id',      '')
+   ->val(  '#additional_parts_add_part_id_name', '')
+   ->focus('#additional_parts_add_part_id_name')
+   ->append('#edit_additional_parts_list tbody', $row)
+   ->hide('#edit_additional_parts_list_empty')
+   ->show('#edit_additional_parts_list')
+   ->render;
+}
+
+sub action_ajax_save {
+  my ($self) = @_;
+
+  my $db = $self->requirement_spec->db;
+  $db->do_transaction(sub {
+    # Make Emacs happy
+    1;
+    my $parts    = $::form->{additional_parts} || [];
+    my $position = 1;
+    $_->{position} = $position++ for @{ $parts };
+
+    $self->requirement_spec->update_attributes(parts => $parts)->load;
+
+    1;
+  }) or do {
+    return $self->js->error(t8('Saving failed. Error message from the database: #1', $db->error))->render;
+  };
+
+  my $html = $self->render('requirement_spec_part/show', { output => 0 }, initially_hidden => !!$::form->{keep_open});
+
+  $self->js
+    ->replaceWith('#additional_parts_list_container', $html)
+    ->action_if(!$::form->{keep_open}, 'remove', '#additional_parts_form_container')
+    ->render;
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  my ($self, %params) = @_;
+  $::auth->assert('requirement_spec_edit');
+}
+
+#
+# helpers
+#
+
+sub init_js { SL::ClientJS->new(controller => $_[0]) }
+
+sub init_requirement_spec {
+  SL::DB::RequirementSpec->new(id => $::form->{requirement_spec_id})->load(
+    with_objects => [ qw(parts parts.part parts.unit) ],
+  );
+}
+
+1;
index d0cb1cb8bdf006676b0d88d4eb8953faa2e036cc..b60ddf66b058625a22f46cf2f32d24030e2476eb 100644 (file)
@@ -83,6 +83,7 @@ use SL::DB::RequirementSpecComplexity;
 use SL::DB::RequirementSpecDependency;
 use SL::DB::RequirementSpecItem;
 use SL::DB::RequirementSpecOrder;
+use SL::DB::RequirementSpecPart;
 use SL::DB::RequirementSpecPicture;
 use SL::DB::RequirementSpecPredefinedText;
 use SL::DB::RequirementSpecRisk;
index 5acc1c7f4802c551deac3886d33e9b96ccf44621..9695077e792a27ea52c1c3d51032b06d9adf626c 100644 (file)
@@ -163,6 +163,7 @@ my %kivitendo_package_names = (
   requirement_spec_item_dependencies   => 'RequirementSpecDependency',
   requirement_spec_items               => 'RequirementSpecItem',
   requirement_spec_orders              => 'RequirementSpecOrder',
+  requirement_spec_parts               => 'RequirementSpecPart',
   requirement_spec_pictures            => 'RequirementSpecPicture',
   requirement_spec_predefined_texts    => 'RequirementSpecPredefinedText',
   requirement_spec_risks               => 'RequirementSpecRisk',
index 2d54d769a523808a73c394d0c424defe82d4ed78..fecd9d35860254df1b0c66cc9590c05ab79ac9bd 100644 (file)
@@ -29,4 +29,11 @@ sub working_copy_filter {
   return (working_copy_id => undef);
 }
 
+sub not_empty_filter {
+  my @tables = qw(requirement_spec_items requirement_spec_text_blocks requirement_spec_parts);
+  my @filter = map { \"id IN (SELECT nef_${_}.requirement_spec_id FROM ${_} nef_${_})" } @tables;
+
+  return (or => \@filter);
+}
+
 1;
diff --git a/SL/DB/Manager/RequirementSpecPart.pm b/SL/DB/Manager/RequirementSpecPart.pm
new file mode 100644 (file)
index 0000000..aa82534
--- /dev/null
@@ -0,0 +1,15 @@
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::RequirementSpecPart;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::RequirementSpecPart' }
+
+__PACKAGE__->make_manager_methods;
+
+1;
index f081af49ee8ba6d4aa1bcfa86c6ad33b4abc1b60..aa55ffa7682ecad2e9a092ee49cf31afe72ae9c9 100644 (file)
@@ -8,6 +8,8 @@ use base qw(SL::DB::Helper::Manager);
 use SL::DB::Helper::Sorted;
 use SL::DB::Helper::Filtered;
 
+use List::Util qw(first);
+
 sub object_class { 'SL::DB::Unit' }
 
 __PACKAGE__->make_manager_methods;
@@ -41,4 +43,23 @@ sub convertible_to_filter {
   return ("${prefix}name" => [ map { $_->name } @{ $unit->convertible_units } ]);
 }
 
+sub all_units {
+  my ($class) = @_;
+  $::request->cache('all_units')->{sorted} //= $class->get_all_sorted;
+}
+
+sub find_h_unit {
+  my ($class) = @_;
+
+  return $::request->cache('unit_manager')->{h_unit} //= first { $_->name =~ m{^(?: Std | h | Stunde )$}x } @{ $class->all_units };
+}
+
+sub time_based_units {
+  my ($class) = @_;
+
+  my $h_unit = $class->find_h_unit;
+  return [] if !$h_unit;
+  return $::request->cache('unit_manager')->{units} //= $h_unit->convertible_units;
+}
+
 1;
index a1efe9a84ac25835fc73cca675f1ffc3ddb49fc3..c4ee930832642766c17be5504c9bfccb5ed697b8 100644 (file)
@@ -35,7 +35,7 @@ __PACKAGE__->meta->columns(
   shipto_id               => { type => 'integer' },
   shipvia                 => { type => 'text' },
   taxincluded             => { type => 'boolean' },
-  taxzone_id              => { type => 'integer' },
+  taxzone_id              => { type => 'integer', not_null => 1 },
   terms                   => { type => 'integer' },
   transaction_description => { type => 'text' },
   transdate               => { type => 'date', default => 'now()' },
@@ -97,6 +97,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { shipto_id => 'shipto_id' },
   },
 
+  taxzone => {
+    class       => 'SL::DB::TaxZone',
+    key_columns => { taxzone_id => 'id' },
+  },
+
   vendor => {
     class       => 'SL::DB::Vendor',
     key_columns => { vendor_id => 'id' },
index b80cee70aa0b28c801b8adb43974ef21be1f8fb1..72aaa4505ede5b8293bfb3ffd1d367348de57d86 100644 (file)
@@ -52,7 +52,7 @@ __PACKAGE__->meta->columns(
   storno                    => { type => 'boolean', default => 'false' },
   storno_id                 => { type => 'integer' },
   taxincluded               => { type => 'boolean' },
-  taxzone_id                => { type => 'integer' },
+  taxzone_id                => { type => 'integer', not_null => 1 },
   terms                     => { type => 'integer', default => '0' },
   transaction_description   => { type => 'text' },
   transdate                 => { type => 'date', default => 'now' },
index 92bb92ffd2e734a1ba91d547a2bb0e07e87a127e..5e67aac75a789452251a290453b4c32b8ac3bc04 100644 (file)
@@ -44,7 +44,7 @@ __PACKAGE__->meta->columns(
   shipto_id               => { type => 'integer' },
   shipvia                 => { type => 'text' },
   taxincluded             => { type => 'boolean' },
-  taxzone_id              => { type => 'integer' },
+  taxzone_id              => { type => 'integer', not_null => 1 },
   transaction_description => { type => 'text' },
   transdate               => { type => 'date', default => 'now' },
   vendor_id               => { type => 'integer' },
@@ -120,6 +120,11 @@ __PACKAGE__->meta->foreign_keys(
     key_columns => { shipto_id => 'shipto_id' },
   },
 
+  taxzone => {
+    class       => 'SL::DB::TaxZone',
+    key_columns => { taxzone_id => 'id' },
+  },
+
   vendor => {
     class       => 'SL::DB::Vendor',
     key_columns => { vendor_id => 'id' },
index 6de5abd2a4021cb7ec329394c95b5de009a5ef06..b3cb0297061fe97cd943223724a2e8a7be924498 100644 (file)
@@ -40,7 +40,7 @@ __PACKAGE__->meta->columns(
   storno                  => { type => 'boolean', default => 'false' },
   storno_id               => { type => 'integer' },
   taxincluded             => { type => 'boolean', default => 'false' },
-  taxzone_id              => { type => 'integer' },
+  taxzone_id              => { type => 'integer', not_null => 1 },
   transaction_description => { type => 'text' },
   transdate               => { type => 'date', default => 'now' },
   type                    => { type => 'text' },
diff --git a/SL/DB/MetaSetup/RequirementSpecPart.pm b/SL/DB/MetaSetup/RequirementSpecPart.pm
new file mode 100644 (file)
index 0000000..b29ae42
--- /dev/null
@@ -0,0 +1,41 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::RequirementSpecPart;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('requirement_spec_parts');
+
+__PACKAGE__->meta->columns(
+  id                  => { type => 'serial', not_null => 1 },
+  description         => { type => 'text', not_null => 1 },
+  part_id             => { type => 'integer', not_null => 1 },
+  position            => { type => 'integer', not_null => 1 },
+  qty                 => { type => 'numeric', not_null => 1, precision => 15, scale => 5 },
+  requirement_spec_id => { type => 'integer', not_null => 1 },
+  unit_id             => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+  part => {
+    class       => 'SL::DB::Part',
+    key_columns => { part_id => 'id' },
+  },
+
+  requirement_spec => {
+    class       => 'SL::DB::RequirementSpec',
+    key_columns => { requirement_spec_id => 'id' },
+  },
+
+  unit => {
+    class       => 'SL::DB::Unit',
+    key_columns => { unit_id => 'id' },
+  },
+);
+
+1;
+;
index 074d4e317b502d02a5cb57351d625b2d6712b937..8013cd94092e156de6d2a0a4cee0e4afd36bfc17 100644 (file)
@@ -80,7 +80,8 @@ sub calculate_invoice_dates {
 
   my $period_len = $self->get_period_length;
   my $cur_date   = $self->first_billing_date || $self->start_date;
-  my $end_date   = $self->end_date           || DateTime->today_local->add(years => 10);
+  my $end_date   = $self->terminated ? $self->end_date : undef;
+  $end_date    //= DateTime->today_local->add(years => 100);
   my $start_date = $params{past_dates} ? undef                       : $self->get_previous_billed_period_start_date;
   $start_date    = $start_date         ? $start_date->add(days => 1) : $cur_date->clone;
 
index e5dae7ebd563dbfabcefd2b5c94bfa959afd85e8..cd603aa6f1c4ef3f05d03eb65e3dc14b8be6d5f3 100644 (file)
@@ -43,6 +43,11 @@ __PACKAGE__->meta->add_relationship(
     class          => 'SL::DB::RequirementSpecOrder',
     column_map     => { id => 'requirement_spec_id' },
   },
+  parts            => {
+    type           => 'one to many',
+    class          => 'SL::DB::RequirementSpecPart',
+    column_map     => { id => 'requirement_spec_id' },
+  },
 );
 
 __PACKAGE__->meta->initialize;
@@ -119,6 +124,14 @@ sub versioned_copies_sorted {
   return \@copies;
 }
 
+sub parts_sorted {
+  my ($self, @rest) = @_;
+
+  croak "This sub is not a writer" if @rest;
+
+  return [ sort { $a->position <=> $b->position } @{ $self->parts } ];
+}
+
 sub create_copy {
   my ($self, %params) = @_;
 
@@ -158,9 +171,9 @@ sub _copy_from {
   my %paste_template_result;
 
   # Clone text blocks and pictures.
-  my $clone_picture = sub {
-    my ($picture) = @_;
-    my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($picture);
+  my $clone_and_reset_position = sub {
+    my ($src_obj) = @_;
+    my $cloned    = Rose::DB::Object::Helpers::clone_and_reset($src_obj);
     $cloned->position(undef);
     return $cloned;
   };
@@ -169,7 +182,7 @@ sub _copy_from {
     my ($text_block) = @_;
     my $cloned       = Rose::DB::Object::Helpers::clone_and_reset($text_block);
     $cloned->position(undef);
-    $cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]);
+    $cloned->pictures([ map { $clone_and_reset_position->($_) } @{ $text_block->pictures_sorted } ]);
     return $cloned;
   };
 
@@ -181,6 +194,11 @@ sub _copy_from {
     $self->add_text_blocks($paste_template_result{text_blocks});
   }
 
+  # Clone additional parts.
+  $paste_template_result{parts} = [ map { $clone_and_reset_position->($_) } @{ $source->parts } ];
+  my $accessor                  = $params->{paste_template} ? "add_parts" : "parts";
+  $self->$accessor($paste_template_result{parts});
+
   # Save new object -- we need its ID for the items.
   $self->save;
 
@@ -519,6 +537,11 @@ column in ascending order. If the C<output_position> parameter is
 given then only the text blocks belonging to that C<output_position>
 are returned.
 
+=item C<parts_sorted>
+
+Returns an array reference of additional parts sorted by their
+positional column in ascending order.
+
 =item C<validate>
 
 Validate values before saving. Returns list or human-readable error
diff --git a/SL/DB/RequirementSpecPart.pm b/SL/DB/RequirementSpecPart.pm
new file mode 100644 (file)
index 0000000..a511d0b
--- /dev/null
@@ -0,0 +1,11 @@
+package SL::DB::RequirementSpecPart;
+
+use strict;
+
+use SL::DB::MetaSetup::RequirementSpecPart;
+use SL::DB::Manager::RequirementSpecPart;
+use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->initialize;
+
+1;
index e8d260c6797d795a4d9cb989dc6cdcbf27011790..f5f1bc14c7ce0c67c23d147d6a3ad649ed78eaad 100644 (file)
@@ -1,5 +1,8 @@
 package SL::DB::Unit;
 
+use List::MoreUtils qw(any);
+
+
 use strict;
 
 use SL::DB::MetaSetup::Unit;
@@ -27,8 +30,8 @@ sub unit_class {
 
 sub convertible_units {
   my $self = shift;
-  my $all_units = scalar(@_) && (ref($_[0]) eq 'ARRAY') ? $_[0] : \@_;
-  $all_units    = SL::DB::Manager::Unit->get_all if !@{ $all_units };
+  my $all_units = scalar(@_) && (ref($_[0]) eq 'ARRAY') ? $_[0] : [ @_ ];
+  $all_units    = SL::DB::Manager::Unit->all_units if ! @{ $all_units };
   return [
     sort { $a->sortkey <=> $b->sortkey }
     grep { $_->unit_class->name eq $self->unit_class->name }
@@ -57,4 +60,10 @@ sub convert_to {
   return $qty * $my_base_factor / $other_base_factor;
 }
 
+sub is_time_based {
+  my ($self) = @_;
+
+  return any { $_->id == $self->id } @{ SL::DB::Manager::Unit->time_based_units };
+}
+
 1;
index 0960ad71f52248a178f9aff077145541382497e0..f91f7ae3dcb66700fa704267f479e289a480f27c 100644 (file)
--- a/SL/DO.pm
+++ b/SL/DO.pm
@@ -250,8 +250,8 @@ sub save {
     $query = qq|SELECT nextval('id')|;
     ($form->{id}) = selectrow_query($form, $dbh, $query);
 
-    $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1))|;
-    do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}));
+    $query = qq|INSERT INTO delivery_orders (id, donumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults LIMIT 1), ?)|;
+    do_query($form, $dbh, $query, $form->{id}, conv_i($form->{employee_id}), $form->{taxzone_id});
   }
 
   my $project_id;
index a36ffd5dfb955c4f0543e8f794a0012173811439..1368df738a27461745b299b2a3a634ccd295c188 100644 (file)
@@ -1034,7 +1034,7 @@ sub parse_template {
                                       %{ $self->{TEMPLATE_DRIVER_OPTIONS} || {} });
 
   # Copy the notes from the invoice/sales order etc. back to the variable "notes" because that is where most templates expect it to be.
-  $self->{"notes"} = $self->{ $self->{"formname"} . "notes" };
+  $self->{"notes"} = $self->{ $self->{"formname"} . "notes" } if exists $self->{ $self->{"formname"} . "notes" };
 
   if (!$self->{employee_id}) {
     $self->{"employee_${_}"} = $myconfig->{$_} for qw(email tel fax name signature);
@@ -3363,15 +3363,15 @@ sub prepare_for_printing {
   my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
   if ($self->{language_id}) {
     ($language_tc, $output_numberformat, $output_dateformat, $output_longdates) = AM->get_language_details(\%::myconfig, $self, $self->{language_id});
-  } else {
-    $output_dateformat   = $::myconfig{dateformat};
-    $output_numberformat = $::myconfig{numberformat};
-    $output_longdates    = 1;
   }
 
-  $self->{myconfig_output_dateformat}   = $output_dateformat;
-  $self->{myconfig_output_longdates}    = $output_longdates;
-  $self->{myconfig_output_numberformat} = $output_numberformat;
+  $output_dateformat   ||= $::myconfig{dateformat};
+  $output_numberformat ||= $::myconfig{numberformat};
+  $output_longdates    //= 1;
+
+  $self->{myconfig_output_dateformat}   = $output_dateformat   // $::myconfig{dateformat};
+  $self->{myconfig_output_longdates}    = $output_longdates    // 1;
+  $self->{myconfig_output_numberformat} = $output_numberformat // $::myconfig{numberformat};
 
   # Retrieve accounts for tax calculation.
   IC->retrieve_accounts(\%::myconfig, $self, map { $_ => $self->{"id_$_"} } 1 .. $self->{rowcount});
index f880f862ec9f94f57f5161a9964bc8296f6dd398..b36e6065a9f53799f49a77459cc3b17364129f04 100644 (file)
--- a/SL/IR.pm
+++ b/SL/IR.pm
@@ -81,7 +81,7 @@ sub post_invoice {
       &reverse_invoice($dbh, $form);
     } else {
       ($form->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('glid')|);
-      do_query($form, $dbh, qq|INSERT INTO ap (id, invnumber, currency_id) VALUES (?, '', (SELECT id FROM currencies WHERE name=?))|, $form->{id}, $form->{currency});
+      do_query($form, $dbh, qq|INSERT INTO ap (id, invnumber, currency_id, taxzone_id) VALUES (?, '', (SELECT id FROM currencies WHERE name=?), ?)|, $form->{id}, $form->{currency}, $form->{taxzone_id});
     }
   }
 
index 51f3c1bec69072402d6ac0308cac7c7dc98c44d3..f429034e8c6bc36b0f47302749348bfe5ec8b95f 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -583,8 +583,8 @@ sub post_invoice {
       $query = qq|SELECT nextval('glid')|;
       ($form->{"id"}) = selectrow_query($form, $dbh, $query);
 
-      $query = qq|INSERT INTO ar (id, invnumber, currency_id) VALUES (?, ?, (SELECT id FROM currencies WHERE name=?))|;
-      do_query($form, $dbh, $query, $form->{"id"}, $form->{"id"}, $form->{currency});
+      $query = qq|INSERT INTO ar (id, invnumber, currency_id, taxzone_id) VALUES (?, ?, (SELECT id FROM currencies WHERE name=?), ?)|;
+      do_query($form, $dbh, $query, $form->{"id"}, $form->{"id"}, $form->{currency}, $form->{taxzone_id});
 
       if (!$form->{invnumber}) {
         my $trans_number   = SL::TransNumber->new(type => $form->{type}, dbh => $dbh, number => $form->{invnumber}, id => $form->{id});
index e2663c513a442afd3beff6c4272704601f28d6c7..279b08cce9bc3a00236995f6b295d2d5d7bcaeac 100644 (file)
--- a/SL/OE.pm
+++ b/SL/OE.pm
@@ -393,8 +393,8 @@ sub save {
     $query = qq|SELECT nextval('id')|;
     ($form->{id}) = selectrow_query($form, $dbh, $query);
 
-    $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults))|;
-    do_query($form, $dbh, $query, $form->{id}, $form->{employee_id});
+    $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults), ?)|;
+    do_query($form, $dbh, $query, $form->{id}, $form->{employee_id}, $form->{taxzone_id});
   }
 
   my $amount    = 0;
index 3b7e86524b584d82e98ac442742f3261c4eff327..a382fd990c19cc261e6bc09a1f72f89ed5b0d088 100644 (file)
@@ -7,6 +7,7 @@ namespace("kivi").setupLocale({
 "Add section":"Abschnitt hinzufügen",
 "Add sub function block":"Unterfunktionsblock hinzufügen",
 "Add text block":"Textblock erfassen",
+"Additional articles actions":"Aktionen zu zusätzlichen Artikeln",
 "Are you sure?":"Sind Sie sicher?",
 "Basic settings actions":"Aktionen zu Grundeinstellungen",
 "Cancel":"Abbrechen",
@@ -44,6 +45,7 @@ namespace("kivi").setupLocale({
 "Paste template":"Vorlage einfügen",
 "Project link actions":"Projektverknüpfungs-Aktionen",
 "Quotations/Orders actions":"Aktionen für Angebote/Aufträge",
+"Remove article":"Artikel entfernen",
 "Requirement spec actions":"Pflichtenheftaktionen",
 "Requirement spec template actions":"Pflichtenheftvorlagen-Aktionen",
 "Revert to version":"Auf Version zurücksetzen",
@@ -68,5 +70,7 @@ namespace("kivi").setupLocale({
 "Transaction description":"Vorgangsbezeichnung",
 "Update":"Erneuern",
 "Update quotation/order":"Auftrag/Angebot aktualisieren",
-"Version actions":"Aktionen für Versionen"
+"Version actions":"Aktionen für Versionen",
+"flat-rate position":"Pauschalposition",
+"time and effort based position":"Aufwandsposition"
 });
index 5078971672bf65dd6fa592413f9727c0c1e6f0d9..d0ec5608ed7865d1216aec866b1481a28e47eb6d 100644 (file)
@@ -153,6 +153,8 @@ ns.initialize_requirement_spec = function(data) {
 
   ns.create_context_menus(data.is_template);
   $('#requirement_spec_tabs').on("tabsbeforeactivate", ns.tabs_before_activate);
+
+  ns.time_based_units = data.time_based_units;
 };
 
 // -------------------------------------------------------------------------
@@ -565,6 +567,31 @@ ns.assign_order_part_id_to_all = function() {
   }).each(function(idx, elt) {
     $(elt).val(order_part_name);
   });
+
+  var unit = $('#quotations_and_orders_order_id').closest('td').data('unit');
+  var text = ns.time_based_units[unit] ? kivi.t8("time and effort based position") : kivi.t8("flat-rate position");
+
+  $('#quotations_and_orders_form [data-unit-column=1]').html(unit);
+  $('#quotations_and_orders_form [data-position-type-column=1]').html(text);
+};
+
+ns.assign_order_part_on_part_picked = function(event, item) {
+  if (!item || !item.unit)
+    return;
+
+  var $elt = $(this),
+      id   = $elt.prop('id');
+
+  if (id == 'quotations_and_orders_order_id')
+    $elt.closest('td').data('unit', item.unit);
+
+  else {
+    var $tr  = $elt.closest('tr');
+    var text = ns.time_based_units[item.unit] ? kivi.t8("time and effort based position") : kivi.t8("flat-rate position");
+
+    $tr.find('[data-unit-column=1]').html(item.unit);
+    $tr.find('[data-position-type-column=1]').html(text);
+  }
 };
 
 // -------------------------------------------------------------------------
@@ -644,6 +671,121 @@ ns.revert_to_versioned_copy_ajax_call = function(key, opt) {
   return true;
 };
 
+// -------------------------------------------------------------------------
+// -------------------------- time/cost estimate ---------------------------
+// -------------------------------------------------------------------------
+
+ns.standard_time_cost_estimate_ajax_call = function(key, opt) {
+  if (key == 'cancel') {
+    if (confirm(kivi.t8('Do you really want to cancel?'))) {
+      $('#time_cost_estimate').show();
+      $('#time_cost_estimate_form_container').remove();
+    }
+    return true;
+  }
+
+  var add_data = '';
+  if (key == 'save_keep_open') {
+    key      = 'save';
+    add_data = 'keep_open=1&';
+  }
+
+  var data = "action=RequirementSpec/ajax_" + key + "_time_and_cost_estimate&" + add_data;
+
+  if (key == 'save')
+    data += $('#edit_time_cost_estimate_form').serialize()
+         +  '&' + $('#current_content_type').serialize()
+         +  '&' + $('#current_content_id').serialize();
+  else
+    data += 'id=' + encodeURIComponent($('#requirement_spec_id').val());
+
+  $.post("controller.pl", data, kivi.eval_json_result);
+
+  return true;
+};
+
+ns.time_cost_estimate_input_key_down = function(event) {
+  if(event.keyCode == 13) {
+    event.preventDefault();
+    ns.standard_time_cost_estimate_ajax_call('save');
+    return false;
+  }
+};
+
+// -------------------------------------------------------------------------
+// -------------------------- additional parts -----------------------------
+// -------------------------------------------------------------------------
+
+ns.standard_additional_parts_ajax_call = function(key, opt) {
+  var add_data = '';
+  if (key == 'save_keep_open') {
+    key      = 'save';
+    add_data = 'keep_open=1&';
+  }
+
+  var data = "action=RequirementSpecPart/ajax_" + key + "&" + add_data + 'requirement_spec_id=' + encodeURIComponent($('#requirement_spec_id').val()) + '&';
+
+  if (key == 'save')
+    data += $('#edit_additional_parts_form').serialize();
+
+  $.post("controller.pl", data, kivi.eval_json_result);
+
+  return true;
+};
+
+ns.prepare_edit_additional_parts_form = function() {
+  $("#edit_additional_parts_list tbody").sortable({
+    distance: 5,
+    handle:   '.dragdrop',
+    helper:   function(event, ui) {
+      ui.children().each(function() {
+        $(this).width($(this).width());
+      });
+      return ui;
+    }
+
+  });
+};
+
+ns.cancel_edit_additional_parts_form = function() {
+  if (confirm(kivi.t8('Do you really want to cancel?'))) {
+    $('#additional_parts_list_container').show();
+    $('#additional_parts_form_container').remove();
+  }
+  return true;
+};
+
+ns.additional_parts_input_key_down = function(event) {
+  if(event.keyCode == 13) {
+    event.preventDefault();
+    ns.standard_additional_parts_ajax_call('save');
+    return false;
+  }
+};
+
+ns.add_additional_part = function() {
+  var part_id = $('#additional_parts_add_part_id').val();
+  if (!part_id || (part_id == ''))
+    return false;
+
+  var rspec_id = $('#requirement_spec_id').val();
+  var data     = 'action=RequirementSpecPart/ajax_add&requirement_spec_id=' + encodeURIComponent(rspec_id) + '&part_id=' + encodeURIComponent(part_id);
+
+  $.post("controller.pl", data, kivi.eval_json_result);
+
+  return true;
+};
+
+ns.delete_additional_part = function(key, opt) {
+  opt.$trigger.remove();
+  if (!$('#edit_additional_parts_list tbody tr').size()) {
+   $('#edit_additional_parts_list_empty').show();
+   $('#edit_additional_parts_list').hide();
+  }
+
+  return true;
+};
+
 // -------------------------------------------------------------------------
 // ------------------------------- tab widget ------------------------------
 // -------------------------------------------------------------------------
@@ -651,8 +793,9 @@ var content_div_ids_for_tab_headers = {
     'tab-header-function-block':     'function-blocks-tab'
   , 'tab-header-basic-settings':     'ui-tabs-1'
   , 'tab-header-time-cost-estimate': 'ui-tabs-2'
-  , 'tab-header-versions':           'ui-tabs-3'
-  , 'tab-header-quotations-orders':  'ui-tabs-4'
+  , 'tab-header-additional-parts':   'ui-tabs-3'
+  , 'tab-header-versions':           'ui-tabs-4'
+  , 'tab-header-quotations-orders':  'ui-tabs-5'
 };
 
 ns.tabs_before_activate = function(event, ui) {
@@ -804,6 +947,35 @@ ns.create_context_menus = function(is_template) {
     }, general_actions)
   });
 
+  $.contextMenu({
+    selector: '.additional-parts-context-menu',
+    items:    $.extend({
+        heading: { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' }
+      , edit:    { name: kivi.t8('Edit'), icon: "edit", callback: kivi.requirement_spec.standard_additional_parts_ajax_call }
+    }, general_actions)
+  });
+
+  var additional_parts_actions = {
+      save:           { name: kivi.t8('Save'),               icon: "save",  callback: kivi.requirement_spec.standard_additional_parts_ajax_call }
+    , save_keep_open: { name: kivi.t8('Save and keep open'), icon: "save",  callback: kivi.requirement_spec.standard_additional_parts_ajax_call }
+    , cancel:         { name: kivi.t8('Cancel'),             icon: "close",  callback: kivi.requirement_spec.cancel_edit_additional_parts_form }
+  };
+
+  $.contextMenu({
+    selector: '.edit-additional-parts-context-menu',
+    items:    $.extend({
+        heading:        { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' }
+    }, additional_parts_actions, general_actions)
+  });
+
+  $.contextMenu({
+    selector: '.edit-additional-parts-row-context-menu',
+    items:    $.extend({
+        heading:        { name: kivi.t8('Additional articles actions'), className: 'context-menu-heading' }
+      , delete:         { name: kivi.t8('Remove article'),     icon: "delete", callback: kivi.requirement_spec.delete_additional_part }
+    }, additional_parts_actions, general_actions)
+  });
+
   $.contextMenu({
     selector: '.quotations-and-orders-context-menu,.quotations-and-orders-order-context-menu',
     items:    $.extend({
@@ -892,3 +1064,9 @@ ns.create_context_menus = function(is_template) {
 };
 
 });                             // end of namespace(...., function() {...
+
+function local_reinit_widgets() {
+  kivi.run_once_for('#quotations_and_orders_order_id,[name="sections[].order_part_id"]', "assign_order_part_on_part_picked", function(elt) {
+    $(elt).on('set_item:PartPicker', kivi.requirement_spec.assign_order_part_on_part_picked);
+  });
+}
index 8a1994ac04e29fd8b29430facc2c8360543db8a1..98e54f613e17236f13877652ebea27ec83c88859 100755 (executable)
@@ -17,6 +17,7 @@ $self->{texts} = {
   '#1 (custom variable)'        => '#1 (benutzerdefinierte Variable)',
   '#1 - Account number #2, bank code #3, #4' => '#1 - Kontonummber #2, BLZ #3, #4',
   '#1 MD'                       => '#1 PT',
+  '#1 additional part(s)'       => '#1 zusätzliche(r) Artikel',
   '#1 h'                        => '#1 h',
   '#1 of #2 importable objects were imported.' => '#1 von #2 importierbaren Objekten wurden importiert.',
   '#1 prices were updated.'     => '#1 Preise wurden aktualisiert.',
@@ -176,6 +177,7 @@ $self->{texts} = {
   'Add new currency'            => 'Neue Währung hinzufügen',
   'Add new custom variable'     => 'Neue benutzerdefinierte Variable erfassen',
   'Add note'                    => 'Notiz erfassen',
+  'Add part'                    => 'Artikel hinzufügen',
   'Add picture'                 => 'Bild hinzufügen',
   'Add picture to text block'   => 'Bild dem Textblock hinzufügen',
   'Add section'                 => 'Abschnitt hinzufügen',
@@ -185,6 +187,8 @@ $self->{texts} = {
   'Add unit'                    => 'Einheit hinzuf&uuml;gen',
   'Added sections and function blocks: #1' => 'Hinzugefügte Abschnitte und Funktionsblöcke: #1',
   'Added text blocks: #1'       => 'Hinzugefügte Textblöcke: #1',
+  'Additional articles'         => 'Zusätzliche Artikel',
+  'Additional articles actions' => 'Aktionen zu zusätzlichen Artikeln',
   'Address'                     => 'Adresse',
   'Admin'                       => 'Administration',
   'Administration'              => 'Administration',
@@ -933,6 +937,7 @@ $self->{texts} = {
   'Edit Vendor Invoice'         => 'Einkaufsrechnung bearbeiten',
   'Edit Warehouse'              => 'Lager bearbeiten',
   'Edit acceptance status'      => 'Abnahmestatus bearbeiten',
+  'Edit additional articles'    => 'Zusätzliche Artikel bearbeiten',
   'Edit article/section assignments' => 'Zuweisung Artikel/Abschnitte bearbeiten',
   'Edit assignment of articles to sections' => 'Zuweisung Artikel zu Abschnitten bearbeiten',
   'Edit background job'         => 'Hintergrund-Job bearbeiten',
@@ -1283,7 +1288,6 @@ $self->{texts} = {
   'Invdate from'                => 'Rechnungen von',
   'Inventory'                   => 'Inventar',
   'Inventory Account'           => 'Warenbestand',
-  'Inventory account'           => '',
   'Inventory quantity must be zero before you can set this assembly obsolete!' => 'Bevor dieses Erzeugnis als ungültig markiert werden kann, muß das Inventar auf Null sein!',
   'Inventory quantity must be zero before you can set this part obsolete!' => 'Bevor diese Ware als ungültig markiert werden kann, muß das Inventar Null sein!',
   'Inventory system'            => 'Warenbuchungsmethode',
@@ -1521,6 +1525,7 @@ $self->{texts} = {
   'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden',
   'No acceptance statuses has been created yet.' => 'Es wurde noch kein Abnahmestatus angelegt.',
   'No action defined.'          => 'Keine Aktion definiert.',
+  'No articles have been added yet.' => 'Es wurden noch keine Artikel hinzugefügt.',
   'No background job has been created yet.' => 'Es wurden noch keine Hintergrund-Jobs angelegt.',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
@@ -1786,6 +1791,7 @@ $self->{texts} = {
   'Plural'                      => 'Plural',
   'Port'                        => 'Port',
   'Portrait'                    => 'Hochformat',
+  'Position type in quotation/order' => 'Positionstyp in Angebot/Auftrag',
   'Post'                        => 'Buchen',
   'Post Payment'                => 'Zahlung buchen',
   'Post and E-mail'             => 'Buchen und E-Mail',
@@ -1939,6 +1945,7 @@ $self->{texts} = {
   'Removal qty'                 => 'Entnahmemenge',
   'Remove'                      => 'Entfernen',
   'Remove Draft'                => 'Entwurf l&ouml;schen',
+  'Remove article'              => 'Artikel entfernen',
   'Remove draft when posting'   => 'Entwurf beim Buchen l&ouml;schen',
   'Removed sections and function blocks: #1' => 'Entfernte Abschnitte und Funktionsblöcke: #1',
   'Removed spoolfiles!'         => 'Druckdateien entfernt!',
@@ -2328,8 +2335,8 @@ $self->{texts} = {
   'The AR transaction #1 has been deleted.' => 'Die Debitorenbuchung #1 wurde gelöscht.',
   'The Bins in Inventory were only a information text field.' => 'Die Lagerplätze unter Stammdaten/Waren sind nur ein informatives Textfeld.',
   'The Bins in master data were only a information text field.' => 'Die Lagerplätze unter Stammdaten/Waren sind nur ein informatives Textfeld.',
-  'The Buchungsgruppe has been created.' => '',
-  'The Buchungsgruppe has been saved.' => '',
+  'The Buchungsgruppe has been created.' => 'Die Buchungsgruppe wurde erstellt.',
+  'The Buchungsgruppe has been saved.' => 'Die Buchungsgruppe wurde gespeichert.',
   'The GL transaction #1 has been deleted.' => 'Die Dialogbuchung #1 wurde gelöscht.',
   'The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.' => 'Der LDAP-Server "#1:#2" ist nicht erreichbar. Bitte &uuml;berpr&uuml;fen Sie die Angaben in config/kivitendo.conf.',
   'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
@@ -2362,8 +2369,8 @@ $self->{texts} = {
   'The base unit does not exist.' => 'Die Basiseinheit existiert nicht.',
   'The base unit relations must not contain loops (e.g. by saying that unit A\'s base unit is B, B\'s base unit is C and C\'s base unit is A) in row %d.' => 'Die Beziehungen der Einheiten d&uuml;rfen keine Schleifen beinhalten (z.B. wenn gesagt wird, dass Einheit As Basiseinheit B, Bs Basiseinheit C und Cs Basiseinheit A ist) in Zeile %d.',
   'The basic client tables have not been created for this client\'s database yet.' => 'Die grundlegenden Mandantentabellen wurden in der für diesen Mandanten konfigurierten Datenbank noch nicht angelegt.',
-  'The buchungsgruppe has been deleted.' => '',
-  'The buchungsgruppe is in use and cannot be deleted.' => '',
+  'The buchungsgruppe has been deleted.' => 'Die Buchungsgruppe wurde gelöscht.',
+  'The buchungsgruppe is in use and cannot be deleted.' => 'Die Buchungsgruppe wird benutzt und kann daher nicht gelöscht werden.',
   'The business has been created.' => 'Der Kunden-/Lieferantentyp wurde erfasst.',
   'The business has been deleted.' => 'Der Kunden-/Lieferantentyp wurde gelöscht.',
   'The business has been saved.' => 'Der Kunden-/Lieferantentyp wurde gespeichert.',
@@ -2609,6 +2616,7 @@ $self->{texts} = {
   'Therefore several settings that had to be made for each user in the past have been consolidated into the client configuration.' => 'Dazu wurden gewisse Einstellungen, die vorher bei jedem Benutzer vorgenommen werden mussten, in die Konfiguration eines Mandanten verschoben.',
   'Therefore the definition of "kg" with the base unit "g" and a factor of 1000 is valid while defining "g" with a base unit of "kg" and a factor of "0.001" is not.' => 'So ist die Definition von "kg" mit der Basiseinheit "g" und dem Faktor 1000 zulässig, die Definition von "g" mit der Basiseinheit "kg" und dem Faktor "0,001" hingegen nicht.',
   'These wrong entries cannot be fixed automatically.' => 'Diese Einträge können nicht automatisch bereinigt werden.',
+  'They will be updated, new ones for additional parts without a line item added automatically.' => 'Diese Positionen werden automatisch aktualisiert bzw. ergänzt, wenn es noch keine Position zu einem zusätzlichen Artikel gibt.',
   'This can be done with the following query:' => 'Dies kann mit der folgenden Datenbankabfrage erreicht werden:',
   'This could have happened for two reasons:' => 'Dies kann aus zwei Gründen geschehen sein:',
   'This customer number is already in use.' => 'Diese Kundennummer wird bereits verwendet.',
@@ -2745,6 +2753,8 @@ $self->{texts} = {
   'Update with section'         => 'Mit Abschnitt aktualisieren',
   'Updated'                     => 'Erneuert am',
   'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
+  'Updating items with additional parts' => 'Positionen für zusätzliche Artikel aktualisieren',
+  'Updating items with sections' => 'Positionen für Abschnitte aktualisieren',
   'Updating prices of existing entry in database' => 'Preis des Eintrags in der Datenbank wird aktualisiert',
   'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.',
   'Uploaded at'                 => 'Hochgeladen um',
@@ -2864,6 +2874,7 @@ $self->{texts} = {
   'You cannot continue before all required modules are installed.' => 'Sie k&ouml;nnen nicht fortfahren, bevor alle ben&ouml;tigten Pakete installiert sind.',
   'You cannot create an invoice for delivery orders for different customers.' => 'Sie können keine Rechnung zu Lieferscheinen für verschiedene Kunden erstellen.',
   'You cannot create an invoice for delivery orders from different vendors.' => 'Sie können keine Rechnung aus Lieferscheinen von verschiedenen Lieferanten erstellen.',
+  'You cannot modify individual assigments from additional articles to line items.' => 'Eine individuelle Zuordnung der zusätzlichen Artikel zu Positionen kann nicht vorgenommen werden.',
   'You cannot paste function blocks or sub function blocks if there is no section.' => 'Sie können keine Funktionsblöcke oder Unterfunktionsblöcke einfügen, wenn es noch keinen Abschnitt gibt.',
   'You do not have the permissions to access this function.' => 'Sie verfügen nicht über die notwendigen Rechte, um auf diese Funktion zuzugreifen.',
   'You have entered or selected the following shipping address for this customer:' => 'Sie haben die folgende Lieferadresse eingegeben oder ausgew&auml;hlt:',
@@ -2966,6 +2977,7 @@ $self->{texts} = {
   'executed'                    => 'ausgeführt',
   'failed'                      => 'fehlgeschlagen',
   'female'                      => 'weiblich',
+  'flat-rate position'          => 'Pauschalposition',
   'follow_up_list'              => 'wiedervorlageliste',
   'for'                         => 'f&uuml;r',
   'for Period'                  => 'für den Zeitraum',
@@ -3092,6 +3104,7 @@ $self->{texts} = {
   'taxkey 0 with taxrate 0 was created.' => 'Steuerschlüssel 0 wurde angelegt.',
   'taxnumber'                   => 'Automatikkonto',
   'terminated'                  => 'gekündigt',
+  'time and effort based position' => 'Aufwandsposition',
   'to (date)'                   => 'bis',
   'to (time)'                   => 'bis',
   'transfer'                    => 'Umlagerung',
diff --git a/sql/Pg-upgrade2/requirement_spec_parts.sql b/sql/Pg-upgrade2/requirement_spec_parts.sql
new file mode 100644 (file)
index 0000000..723eab6
--- /dev/null
@@ -0,0 +1,17 @@
+-- @tag: requirement_spec_parts
+-- @description: Artikelzuweisung zu Pflichtenheften
+-- @depends: release_3_1_0
+CREATE TABLE requirement_spec_parts (
+  id                  SERIAL         NOT NULL,
+  requirement_spec_id INTEGER        NOT NULL,
+  part_id             INTEGER        NOT NULL,
+  unit_id             INTEGER        NOT NULL,
+  qty                 NUMERIC(15, 5) NOT NULL,
+  description         TEXT           NOT NULL,
+  position            INTEGER        NOT NULL,
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id),
+  FOREIGN KEY (part_id)             REFERENCES parts             (id),
+  FOREIGN KEY (unit_id)             REFERENCES units             (id)
+);
diff --git a/sql/Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql b/sql/Pg-upgrade2/taxzone_id_in_oe_delivery_orders.sql
new file mode 100644 (file)
index 0000000..00a654e
--- /dev/null
@@ -0,0 +1,16 @@
+-- @tag: taxzone_id_in_oe_delivery_orders
+-- @description: Werte für Inland in Spalte taxzone_id in Tabellen oe und delivery_orders in Foreign Key zu tax_zones konvertieren; NULL-Werte in ap/ar verhindern; Spalten NOT NULL setzen
+-- @depends: change_taxzone_id_0
+
+UPDATE oe              SET taxzone_id = (SELECT id FROM tax_zones WHERE description = 'Inland') WHERE (taxzone_id = 0) OR (taxzone_id IS NULL);
+UPDATE delivery_orders SET taxzone_id = (SELECT id FROM tax_zones WHERE description = 'Inland') WHERE (taxzone_id = 0) OR (taxzone_id IS NULL);
+UPDATE ar              SET taxzone_id = (SELECT id FROM tax_zones WHERE description = 'Inland') WHERE (taxzone_id = 0) OR (taxzone_id IS NULL);
+UPDATE ap              SET taxzone_id = (SELECT id FROM tax_zones WHERE description = 'Inland') WHERE (taxzone_id = 0) OR (taxzone_id IS NULL);
+
+ALTER TABLE oe              ALTER COLUMN taxzone_id SET NOT NULL;
+ALTER TABLE delivery_orders ALTER COLUMN taxzone_id SET NOT NULL;
+ALTER TABLE ar              ALTER COLUMN taxzone_id SET NOT NULL;
+ALTER TABLE ap              ALTER COLUMN taxzone_id SET NOT NULL;
+
+ALTER TABLE oe              ADD CONSTRAINT oe_taxzone_id_fkey              FOREIGN KEY (taxzone_id) REFERENCES tax_zones (id);
+ALTER TABLE delivery_orders ADD CONSTRAINT delivery_orders_taxzone_id_fkey FOREIGN KEY (taxzone_id) REFERENCES tax_zones (id);
index e2c6e672f4ad50f84c0ad3bdfb4cd62623716643..053c22954e761b35a1644bf71ac76f1b886831bc 100644 (file)
    <td>[% HTML.escape(bank) %]</td>
   </tr>
 
+  <tr>
+   <td align="right">[% 'IBAN' | $T8 %]</td>
+   <td>[% HTML.escape(iban) %]</td>
+  </tr>
+
+  <tr>
+   <td align="right">[% 'BIC' | $T8 %]</td>
+   <td>[% HTML.escape(bic) %]</td>
+  </tr>
   <tr>
    <td align="right">[% IF is_customer %][% 'Customer type' | $T8 %][% ELSE %][% 'Vendor type' | $T8 %][% END %]</td>
    <td>[% HTML.escape(business) %]</td>
index 3399d590b393ad19e21781b9fe3eb9615d758fb4..3ddeb2d1c492947ecff972d19262cd983a413928 100644 (file)
@@ -44,6 +44,7 @@
   [% SET front    = template.text_blocks_sorted(output_position=0) %]
   [% SET sections = template.sections_sorted %]
   [% SET back     = template.text_blocks_sorted(output_position=1) %]
+  [% SET parts    = template.parts_sorted %]
   <td colspan="4">
    [%- LxERP.t8("What this template contains") %]:<br>
    <ul>
     [%- END %]
 
     [% PROCESS text_blocks blocks=back  title=LxERP.t8("#1 text block(s) back",  back.size)  %]
+
+    [%- IF parts.size %]
+    <li>[%- LxERP.t8("#1 additional part(s)", parts.size) %]:
+     <ol>
+      [%- FOREACH part = parts %]
+       <li>[%- HTML.escape(part.part.description) %]: [%- HTML.escape(part.qty_as_number) %] [% HTML.escape(part.unit.name) %]</li>
+      [%- END %]
+     </ol>
+    </li>
+    [%- END %]
    </ul>
   </td>
  </tr>
index c41e87d84ca9895f5852cea38afb9ad5a78b1878..c329a4e4d600dfc0c5e759ece7a824925f208466 100644 (file)
@@ -1,4 +1,4 @@
-[%- USE JSON -%][%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE P -%]
+[%- USE JSON -%][%- USE HTML %][%- USE L %][%- USE LxERP %][%- USE P -%][%- USE JavaScript -%]
 [% SET sections = SELF.requirement_spec.sections_sorted || [] %]
 
 [%- INCLUDE 'common/flash.html' %]
@@ -12,6 +12,7 @@
   <li id="tab-header-function-block"><a href="#function-blocks-tab">[%- LxERP.t8("Content") %]</a></li>
   <li id="tab-header-basic-settings"><a href="controller.pl?action=RequirementSpec/ajax_show_basic_settings&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Basic settings") %]</a></li>
   <li id="tab-header-time-cost-estimate"><a href="controller.pl?action=RequirementSpec/ajax_show_time_and_cost_estimate&id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Time and cost estimate") %]</a></li>
+  <li id="tab-header-additional-parts"><a href="controller.pl?action=RequirementSpecPart/show&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Additional articles") %]</a></li>
   [%- UNLESS SELF.requirement_spec.is_template %]
    <li id="tab-header-versions"><a href="controller.pl?action=RequirementSpecVersion/list&requirement_spec_id=[% HTML.url(SELF.requirement_spec.id) %]">[%- LxERP.t8("Versions") %]</a></li>
    <li id="tab-header-quotations-orders"><a href="[% SELF.url_for(controller='RequirementSpecOrder', action='list', requirement_spec_id=SELF.requirement_spec.id) %]">[%- LxERP.t8("Quotations and orders") %]</a></li>
@@ -94,6 +95,11 @@ $(function() {
 [% IF SELF.requirement_spec_item %]
     , initially_selected_node: '#fb-[% SELF.requirement_spec_item.id %]'
 [% END %]
+    , time_based_units: {
+      [% FOREACH unit = SELF.time_based_units %]
+       [% UNLESS loop.first %], [% END %] "[% JavaScript.escape(unit.name) %]": true
+      [% END %]
+    }
   });
 });
 
index 9990c55eafec329d87c00cdfd566f858e6033f29..634cb2446eaf0601a240776c607634f58a05684e 100644 (file)
@@ -17,8 +17,8 @@
 
   <tr>
    <td>[% LxERP.t8("Assign the following article to all sections") %]:</td>
-   <td>
-    [% P.part_picker('quotations_and_orders_dummy', INSTANCE_CONF.get_requirement_spec_section_order_part_id, convertible_unit=SELF.h_unit_name, id='quotations_and_orders_order_id', style=style) %]
+   <td data-unit="[% HTML.escape(SELF.section_order_part.unit) %]">
+    [% P.part_picker('quotations_and_orders_dummy', SELF.section_order_part.id, id='quotations_and_orders_order_id', style=style) %]
     [% L.button_tag('kivi.requirement_spec.assign_order_part_id_to_all()', LxERP.t8('Assign article')) %]
    </td>
   </tr>
@@ -31,6 +31,8 @@
     <th>[% LxERP.t8("Title") %]</th>
     <th>[% LxERP.t8("Description") %]</th>
     <th>[% LxERP.t8("Article") %]</th>
+    <th>[% LxERP.t8("Unit") %]</th>
+    <th>[% LxERP.t8("Position type in quotation/order") %]</th>
    </tr>
   </thead>
 
     <td>[% HTML.escape(section.fb_number) %]</td>
     <td>[% HTML.escape(section.title) %]</td>
     <td>[% HTML.escape(P.truncate(section.description_as_stripped_html)) %]</td>
-    <td>[% P.part_picker('sections[].order_part_id', section.order_part_id, convertible_unit=SELF.h_unit_name, id='quotations_and_orders_sections_order_pard_id_' _ loop.count, style=style) %]</td>
+    <td>[% P.part_picker('sections[].order_part_id', section.order_part_id, id='quotations_and_orders_sections_order_pard_id_' _ loop.count, style=style) %]</td>
+    <td data-unit-column=1>[% HTML.escape(section.order_part.unit) %]</td>
+    <td data-position-type-column=1>
+     [% IF section.order_part_id && section.order_part.unit_obj.is_time_based %]
+      [% LxERP.t8("time and effort based position") %]
+     [% ELSIF section.order_part_id %]
+      [% LxERP.t8("flat-rate position") %]
+     [% END %]
+    </td>
    </tr>
    [% END %]
   </tbody>
index b4bef1dfca252aa65bd687a3d153e7c9186d4bce..4f8278d19f2b9b320cf8834d22bf483aee5670a7 100644 (file)
@@ -15,6 +15,8 @@
     <th>[% LxERP.t8("Title") %]</th>
     <th>[% LxERP.t8("Description") %]</th>
     <th>[% LxERP.t8("Article") %]</th>
+    <th>[% LxERP.t8("Unit") %]</th>
+    <th>[% LxERP.t8("Position type in quotation/order") %]</th>
    </tr>
   </thead>
 
        [% LxERP.t8("no article assigned yet") %]
       [% END %]
      </td>
+     <td>[% HTML.escape(section.order_part.unit) %]</td>
+     <td>
+      [% IF section.order_part_id && section.order_part.unit_obj.is_time_based %]
+       [% LxERP.t8("time and effort based position") %]
+      [% ELSIF section.order_part_id %]
+       [% LxERP.t8("flat-rate position") %]
+      [% END %]
+     </td>
     </tr>
    [% END %]
   </tbody>
index 13b37980a5e11145ce8ba8a93a4a257854cad95c..34b890eead04ea5682c5e2d6681b5307e003c2ce 100644 (file)
@@ -13,6 +13,8 @@
  </h2>
 
  <form id="quotations_and_orders_form">
+  <h3>[% LxERP.t8("Updating items with sections") %]</h3>
+
   [% L.hidden_tag("rs_order_id", SELF.rs_order.id, no_id=1) %]
 
   <table style="width: 100%">
    [% LxERP.t8("Sections that are not assigned to any of the items above will be added as new positions.") %]
   </p>
 
+  <h3>[% LxERP.t8("Updating items with additional parts") %]</h3>
+
+  <p>
+   [% LxERP.t8("You cannot modify individual assigments from additional articles to line items.") %]
+   [% LxERP.t8("They will be updated, new ones for additional parts without a line item added automatically.") %]
+  </p>
+
   <p>
    [% L.button_tag("kivi.requirement_spec.standard_quotation_order_ajax_call('do_update')", LxERP.t8('Update')) %]
   </p>
diff --git a/templates/webpages/requirement_spec_part/_edit.html b/templates/webpages/requirement_spec_part/_edit.html
new file mode 100644 (file)
index 0000000..c1b2795
--- /dev/null
@@ -0,0 +1,38 @@
+[%- USE LxERP -%][%- USE L -%][%- USE P -%]
+[% SET parts = SELF.requirement_spec.parts_sorted %]
+
+<div id="additional_parts_form_container" class="edit-additional-parts-context-menu">
+
+ <h2>[% LxERP.t8("Edit additional articles") %]</h2>
+
+ <div>
+  [% LxERP.t8("Add part") %]:
+  [% P.part_picker('additional_parts_add_part_id', '', style="width: 300px") %]
+  [% L.button_tag('kivi.requirement_spec.add_additional_part()', LxERP.t8('Add part')) %]
+ </div>
+
+ <form method="post" id="edit_additional_parts_form">
+  <div id="edit_additional_parts_list_empty"[% IF parts.size %] style="display: none;"[% END %]>
+   [% LxERP.t8("No articles have been added yet.") %]
+  </div>
+
+  <table id="edit_additional_parts_list"[% IF !parts.size %] style="display: none;"[% END %]>
+   <thead>
+    <tr class="listheading">
+     <th></th>
+     <th>[%- LxERP.t8("Part Number") %]</th>
+     <th>[%- LxERP.t8("Description") %]</th>
+     <th>[%- LxERP.t8("Qty") %]</th>
+    </tr>
+   </thead>
+
+   <tbody>
+    [%- FOREACH part = parts %]
+     [%- INCLUDE 'requirement_spec_part/_part.html' part=part %]
+    [%- END %]
+   </tbody>
+  </table>
+
+   [% L.button_tag("kivi.requirement_spec.standard_additional_parts_ajax_call('save')", LxERP.t8("Save")) %]
+  </form>
+</div>
diff --git a/templates/webpages/requirement_spec_part/_part.html b/templates/webpages/requirement_spec_part/_part.html
new file mode 100644 (file)
index 0000000..6136de0
--- /dev/null
@@ -0,0 +1,14 @@
+[%- USE HTML -%][%- USE L -%][%- USE LxERP -%]
+<tr class="listrow edit-additional-parts-row-context-menu">
+ <td align="center">
+  [% L.hidden_tag("additional_parts[+].part_id", part.part.id) %]
+  [% L.hidden_tag("additional_parts[].id", part.id) %]
+  [% L.img_tag(src="image/updown.png", alt=LxERP.t8("reorder item"), class="dragdrop") %]
+ </td>
+ <td>[% HTML.escape(part.part.partnumber) %]</td>
+ <td>[% L.input_tag("additional_parts[].description", part.description, size="30") %]</td>
+ <td>
+  [% L.input_tag("additional_parts[].qty_as_number", part.qty_as_number, size="10") %]
+  [% L.select_tag("additional_parts[].unit_id", part.unit.convertible_units, title_key="name", default=part.unit.id) %]
+ </td>
+</tr>
diff --git a/templates/webpages/requirement_spec_part/show.html b/templates/webpages/requirement_spec_part/show.html
new file mode 100644 (file)
index 0000000..128c7e0
--- /dev/null
@@ -0,0 +1,31 @@
+[%- USE LxERP -%][%- USE L -%][%- USE P -%][%- USE HTML -%]
+[% SET parts = SELF.requirement_spec.parts_sorted %]
+
+<div id="additional_parts_list_container" class="additional-parts-context-menu"[% IF initially_hidden %] style="display: none;"[% END %]>
+
+ <h2>[% LxERP.t8("Additional articles") %]</h2>
+
+ <div id="additional_parts_list_empty"[% IF parts.size %] style="display: none;"[% END %]>
+  [% LxERP.t8("No articles have been added yet.") %]
+ </div>
+
+ <table id="additional_parts_list"[% IF !parts.size %] style="display: none;"[% END %]>
+  <thead>
+   <tr class="listheading">
+    <th>[%- LxERP.t8("Part Number") %]</th>
+    <th>[%- LxERP.t8("Description") %]</th>
+    <th>[%- LxERP.t8("Qty") %]</th>
+   </tr>
+  </thead>
+
+  <tbody>
+   [% FOREACH part = parts %]
+    <tr class="listrow">
+     <td>[% HTML.escape(part.part.partnumber) %]</td>
+     <td>[% HTML.escape(part.description) %]</td>
+     <td valign="right">[% HTML.escape(part.qty_as_number) %] [% HTML.escape(part.unit.name) %]</td>
+    </tr>
+   [% END %]
+  </tbody>
+ </table>
+</div>