($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});
}
} 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;
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;
$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 {
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} } ];
(
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');
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);
}
$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);
}
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 {
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');
$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);
}
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;
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);
}
}
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 ]);
}
# 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 {
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;
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,
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,
--- /dev/null
+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;
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;
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',
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;
--- /dev/null
+# 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;
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;
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;
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()' },
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' },
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' },
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' },
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' },
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' },
--- /dev/null
+# 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;
+;
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;
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;
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) = @_;
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;
};
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;
};
$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;
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
--- /dev/null
+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;
package SL::DB::Unit;
+use List::MoreUtils qw(any);
+
+
use strict;
use SL::DB::MetaSetup::Unit;
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 }
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;
$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;
%{ $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);
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});
&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});
}
}
$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});
$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;
"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",
"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",
"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"
});
ns.create_context_menus(data.is_template);
$('#requirement_spec_tabs').on("tabsbeforeactivate", ns.tabs_before_activate);
+
+ ns.time_based_units = data.time_based_units;
};
// -------------------------------------------------------------------------
}).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);
+ }
};
// -------------------------------------------------------------------------
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 ------------------------------
// -------------------------------------------------------------------------
'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) {
}, 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({
};
}); // 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);
+ });
+}
'#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.',
'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',
'Add unit' => 'Einheit hinzufü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',
'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',
'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',
'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.',
'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',
'Removal qty' => 'Entnahmemenge',
'Remove' => 'Entfernen',
'Remove Draft' => 'Entwurf löschen',
+ 'Remove article' => 'Artikel entfernen',
'Remove draft when posting' => 'Entwurf beim Buchen löschen',
'Removed sections and function blocks: #1' => 'Entfernte Abschnitte und Funktionsblöcke: #1',
'Removed spoolfiles!' => 'Druckdateien entfernt!',
'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 überprüfen Sie die Angaben in config/kivitendo.conf.',
'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
'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ü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.',
'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.',
'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',
'You cannot continue before all required modules are installed.' => 'Sie können nicht fortfahren, bevor alle benö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ählt:',
'executed' => 'ausgeführt',
'failed' => 'fehlgeschlagen',
'female' => 'weiblich',
+ 'flat-rate position' => 'Pauschalposition',
'follow_up_list' => 'wiedervorlageliste',
'for' => 'für',
'for Period' => 'für den Zeitraum',
'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',
--- /dev/null
+-- @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)
+);
--- /dev/null
+-- @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);
<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>
[% 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>
-[%- 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' %]
<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>
[% 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 %]
+ }
});
});
<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>
<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>
<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>
</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>
--- /dev/null
+[%- 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>
--- /dev/null
+[%- 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>
--- /dev/null
+[%- 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>