sub _calculate_dates {
my ($config) = @_;
- return $config->calculate_invoice_dates(end_date => DateTime->today_local->add(days => 1));
+ return $config->calculate_invoice_dates(end_date => DateTime->today_local);
}
sub _send_email {
return unless $template;
my $email_template = $config{periodic_invoices}->{email_template};
- my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/periodic_invoices_email.txt" );
+ my $filename = $email_template || ( (SL::DB::Default->get->templates || "templates/webpages") . "/oe/periodic_invoices_email.txt" );
my %params = ( POSTED_INVOICES => $posted_invoices,
PRINTED_INVOICES => $printed_invoices );
"email" => "ct.email",
"street" => "ct.street",
"taxnumber" => "ct.taxnumber",
- "business" => "ct.business",
+ "business" => "b.description",
"invnumber" => "ct.invnumber",
"ordnumber" => "ct.ordnumber",
"quonumber" => "ct.quonumber",
"city" => "ct.city",
"country" => "ct.country",
"discount" => "ct.discount",
- "salesman" => "e.name"
+ "salesman" => "e.name",
+ "payment" => "pt.description"
);
$form->{sort} ||= "name";
}
my $query =
- qq|SELECT ct.*, b.description AS business, e.name as salesman | .
+ qq|SELECT ct.*, b.description AS business, e.name as salesman, |.
+ qq| pt.description as payment | .
(qq|, NULL AS invnumber, NULL AS ordnumber, NULL AS quonumber, NULL AS invid, NULL AS module, NULL AS formtype, NULL AS closed | x!! $join_records) .
qq|FROM $cv ct | .
qq|LEFT JOIN business b ON (ct.business_id = b.id) | .
qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | .
+ qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | .
qq|WHERE $where|;
my @saved_values = @values;
push(@values, @saved_values);
$query .=
qq| UNION | .
- qq|SELECT ct.*, b.description AS business, e.name as salesman, | .
+ qq|SELECT ct.*, b.description AS business, e.name as salesman, |.
+ qq| pt.description as payment, | .
qq| a.invnumber, a.ordnumber, a.quonumber, a.id AS invid, | .
qq| '$module' AS module, 'invoice' AS formtype, | .
qq| (a.amount = a.paid) AS closed | .
qq|JOIN $ar a ON (a.${cv}_id = ct.id) | .
qq|LEFT JOIN business b ON (ct.business_id = b.id) | .
qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | .
+ qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | .
qq|WHERE $where AND (a.invoice = '1')|;
}
push(@values, @saved_values);
$query .=
qq| UNION | .
- qq|SELECT ct.*, b.description AS business, e.name as salesman, | .
+ qq|SELECT ct.*, b.description AS business, e.name as salesman, |.
+ qq| pt.description as payment, | .
qq| ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | .
qq| 'oe' AS module, 'order' AS formtype, o.closed | .
qq|FROM $cv ct | .
qq|JOIN oe o ON (o.${cv}_id = ct.id) | .
qq|LEFT JOIN business b ON (ct.business_id = b.id) | .
qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | .
+ qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | .
qq|WHERE $where AND (o.quotation = '0')|;
}
$query .=
qq| UNION | .
qq|SELECT ct.*, b.description AS business, e.name as salesman, | .
+ qq| pt.description as payment, | .
qq| ' ' AS invnumber, o.ordnumber, o.quonumber, o.id AS invid, | .
qq| 'oe' AS module, 'quotation' AS formtype, o.closed | .
qq|FROM $cv ct | .
qq|JOIN oe o ON (o.${cv}_id = ct.id) | .
qq|LEFT JOIN business b ON (ct.business_id = b.id) | .
qq|LEFT JOIN employee e ON (ct.salesman_id = e.id) | .
+ qq|LEFT JOIN payment_terms pt ON (ct.payment_id = pt.id) | .
qq|WHERE $where AND (o.quotation = '1')|;
}
}
--- /dev/null
+package SL::CTI;
+
+use strict;
+
+use String::ShellQuote;
+
+use SL::MoreCommon qw(uri_encode);
+
+sub call {
+ my ($class, %params) = @_;
+
+ my $config = $::lx_office_conf{cti} || {};
+ my $command = $config->{dial_command} || die $::locale->text('Dial command missing in kivitendo configuration\'s [cti] section');
+ my $external_prefix = $params{internal} ? '' : ($config->{external_prefix} // '');
+
+ my %command_args = (
+ phone_extension => $::myconfig{phone_extension} || die($::locale->text('Phone extension missing in user configuration')),
+ phone_password => $::myconfig{phone_password} || die($::locale->text('Phone password missing in user configuration')),
+ number => $external_prefix . $class->sanitize_number(%params),
+ );
+
+ foreach my $key (keys %command_args) {
+ my $value = shell_quote($command_args{$key});
+ $command =~ s{<\% ${key} \%>}{$value}gx;
+ }
+
+ return `$command`;
+}
+
+sub call_link {
+ my ($class, %params) = @_;
+
+ return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : '');
+}
+
+sub sanitize_number {
+ my ($class, %params) = @_;
+
+ my $config = $::lx_office_conf{cti} || {};
+ my $idp = $config->{international_dialing_prefix} // '00';
+
+ my $number = $params{number} // '';
+ $number =~ s/[^0-9+\.-]//g; # delete unsupported characters
+ my $countrycode = $number =~ s/^(?: $idp | \+ ) ( \d{2} )//x ? $1 : ''; # TODO: countrycodes can have more or less than 2 digits
+ $number =~ s/^0//x if $countrycode; # kill non standard optional zero after global identifier
+
+ return '' unless $number;
+
+ return ($countrycode ? $idp . $countrycode : '') . $number;
+}
+
+1;
@filters;
my %status = (
- failed => $::locale->text('failed'),
+ failure => $::locale->text('failed'),
success => $::locale->text('succeeded'),
);
push @filter_strings, $status{ $filter->{'status:eq_ignore_empty'} } if $filter->{'status:eq_ignore_empty'};
if (!ref $file_name_or_content) {
$::locale->with_raw_io(\*STDOUT, sub { print while <$file> });
$file->close;
+ unlink $file_name_or_content if $params{unlink};
} else {
$::locale->with_raw_io(\*STDOUT, sub { print $$file_name_or_content });
}
=item * C<name> -- the name presented to the browser; defaults to
C<$file_name>; mandatory if C<$file_name_or_content> is a reference
+=item * C<unlink> -- if trueish and C<$file_name_or_content> refers to
+a file name then unlink the file after it has been sent to the browser
+(e.g. for temporary files)
+
=back
=item C<url_for $url>
--- /dev/null
+package SL::Controller::CTI;
+
+use strict;
+
+use SL::CTI;
+use SL::DB::AuthUserConfig;
+use SL::Helper::Flash;
+use SL::Locale::String;
+
+use parent qw(SL::Controller::Base);
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(internal_extensions) ],
+);
+
+sub action_call {
+ my ($self) = @_;
+
+ eval {
+ my $res = SL::CTI->call(number => $::form->{number}, internal => $::form->{internal});
+ flash('info', t8('Calling #1 now', $::form->{number}));
+ 1;
+ } or do {
+ flash('error', $@);
+ };
+
+ $self->render('cti/calling');
+}
+
+sub action_list_internal_extensions {
+ my ($self) = @_;
+
+ $self->render('cti/list_internal_extensions', title => t8('Internal Phone List'));
+}
+
+#
+# filters
+#
+
+sub init_internal_extensions {
+ my ($self) = @_;
+
+ my $user_configs = SL::DB::Manager::AuthUserConfig->get_all(
+ where => [
+ cfg_key => 'phone_extension',
+ '!cfg_value' => undef,
+ '!cfg_value' => '',
+ ],
+ with_objects => [ qw(user) ],
+ );
+
+ my %users;
+ foreach my $config (@{ $user_configs }) {
+ $users{$config->user_id} ||= {
+ name => $config->user->get_config_value('name') || $config->user->login,
+ phone_extension => $config->cfg_value,
+ call_link => SL::CTI->call_link(number => $config->cfg_value, internal => 1),
+ };
+ }
+
+ return [
+ sort { lc($a->{name}) cmp lc($b->{name}) } values %users
+ ];
+}
+
+1;
$::form->{column} ? ($::form->{column} => $query) : (or => [ customernumber => $query, name => $query ])
);
+ push @filter, (or => [ obsolete => undef, obsolete => 0 ]) if !$::form->{obsolete};
+
my $customers = SL::DB::Manager::Customer->get_all(query => [ @filter ], limit => $limit);
my $value_col = $::form->{column} || 'name';
$self->{cv}->name($name);
}
+sub home_address_for_google_maps {
+ my ($self) = @_;
+
+ my $address = $::instance_conf->get_address // '';
+ $address =~ s{^\s+|\s+$|\r+}{}g;
+ $address =~ s{\n+}{,}g;
+ $address =~ s{\s+}{ }g;
+
+ return $address;
+}
+
1;
--- /dev/null
+package SL::Controller::LiquidityProjection;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Locale::String;
+use SL::LiquidityProjection;
+use SL::Util qw(_hashify);
+
+__PACKAGE__->run_before('check_auth');
+
+use Rose::Object::MakeMethods::Generic (
+ scalar => [ qw(liquidity) ],
+ 'scalar --get_set_init' => [ qw(oe_report_columns_str) ],
+);
+
+
+#
+# actions
+#
+
+sub action_show {
+ my ($self) = @_;
+
+ $self->liquidity(SL::LiquidityProjection->new(%{ $::form->{params} })->create) if $::form->{params};
+
+ $::form->{params} ||= {
+ months => 6,
+ type => 1,
+ salesman => 1,
+ buchungsgruppe => 1,
+ };
+
+ $self->render('liquidity_projection/show', title => t8('Liquidity projection'));
+}
+
+#
+# filters
+#
+
+sub check_auth { $::auth->assert('report') }
+sub init_oe_report_columns_str { join '&', map { "$_=Y" } qw(open delivered notdelivered l_ordnumber l_transdate l_reqdate l_name l_employee l_salesman l_netamount l_amount l_transaction_description) }
+
+#
+# helpers
+#
+
+sub link_to_old_orders {
+ my $self = shift;
+ my %params = _hashify(0, @_);
+
+ my $reqdate = $params{reqdate};
+ my $months = $params{months} * 1;
+
+ my $fields = '';
+
+ if ($reqdate eq 'old') {
+ $fields .= '&reqdate_unset_or_old=Y';
+
+ } elsif ($reqdate eq 'future') {
+ my @now = localtime;
+ $fields .= '&reqdatefrom=' . $self->iso_to_display(SL::LiquidityProjection::_the_date($now[5] + 1900, $now[4] + 1 + $months) . '-01');
+
+ } else {
+ $reqdate =~ m/(\d+)-(\d+)/;
+ $fields .= '&reqdatefrom=' . $self->iso_to_display($reqdate . '-01');
+ $fields .= '&reqdateto=' . $self->iso_to_display($reqdate . sprintf('-%02d', DateTime->last_day_of_month(year => $1, month => $2)->day));
+
+ }
+
+ return "oe.pl?action=orders&type=sales_order&vc=customer&" . $self->oe_report_columns_str . $fields;
+}
+
+sub iso_to_display {
+ my ($self, $date) = @_;
+
+ $::locale->reformat_date({ dateformat => 'yyyy-mm-dd' }, $date, $::myconfig{dateformat});
+}
+
+1;
my %states = (
session => { warning => t8('The session has expired. Please log in again.') },
password => { error => t8('Incorrect username or password or no access to selected client!') },
+ action => { warning => t8('The action is missing or invalid.') },
);
return %{ $states{$_[0]} || {} };
description => $_->description,
type => $_->type,
unit => $_->unit,
+ cvars => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } },
}
} @{ $self->parts }; # neato: if exact match triggers we don't even need the init_parts
sub get_active_taxkey {
my ($self, $date) = @_;
$date ||= DateTime->today_local;
+
+ my $cache = $::request->cache("get_active_taxkey")->{$date} //= {};
+ return $cache->{$self->id} if $cache->{$self->id};
+
require SL::DB::TaxKey;
- return SL::DB::Manager::TaxKey->get_all(query => [ and => [ chart_id => $self->id,
- startdate => { le => $date } ] ],
- sort_by => "startdate DESC")->[0];
+ return $cache->{$self->id} = SL::DB::Manager::TaxKey->get_all(
+ query => [ and => [ chart_id => $self->id,
+ startdate => { le => $date } ] ],
+ sort_by => "startdate DESC")->[0];
}
1;
sub _ensure_config {
my ($self) = @_;
- return $self->config if $self->config;
+ return $self->config if defined $self->{config};
return undef if !defined $self->config_id;
- $self->config( SL::DB::CustomVariableConfig->new(id => $self->config_id)->load );
+
+ no warnings 'once';
+ return $::request->cache('config_by_id')->{$self->config_id} //= SL::DB::CustomVariableConfig->new(id => $self->config_id)->load;
}
sub parse_value {
sub value_as_text {
my $self = $_[0];
- my $type = $self->config->type;
+ my $cfg = $self->_ensure_config;
+ my $type = $cfg->type;
die 'not an accessor' if @_ > 1;
} elsif ($type eq 'timestamp') {
return $::locale->reformat_date( { dateformat => 'yy-mm-dd' }, $self->timestamp_value->ymd, $::myconfig{dateformat});
} elsif ($type eq 'number') {
- return $::form->format_amount(\%::myconfig, $self->number_value, $self->config->processed_options->{PRECISION});
+ return $::form->format_amount(\%::myconfig, $self->number_value, $cfg->processed_options->{PRECISION});
} elsif ( $type eq 'customer' ) {
require SL::DB::Customer;
require SL::DB::CustomVariableValidity;
my $query = [config_id => $self->config_id, trans_id => $self->trans_id];
- return SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0;
+ return (SL::DB::Manager::CustomVariableValidity->get_all_count(query => $query) == 0) ? 1 : 0;
}
1;
use strict;
+use List::MoreUtils qw(any);
+
use SL::DB::MetaSetup::CustomVariableConfig;
use SL::DB::Manager::CustomVariableConfig;
use SL::DB::Helper::ActsAsList;
return $self->processed_flags()->{$flag};
}
+sub type_dependent_default_value {
+ my ($self) = @_;
+
+ return $self->default_value if $self->type ne 'select';
+ return (any { $_ eq $self->default_value } @{ $self->processed_options }) ? $self->default_value : $self->processed_options->[0];
+}
+
1;
# value needs config
$inherited_value
? $cvar->value($inherited_value)
- : $cvar->value($params{config}->default_value);
+ : $cvar->value($params{config}->type_dependent_default_value);
return $cvar;
}
ORDER BY ${wanted}_table, ${wanted}_id, depth ASC;
my $links = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id, $self->meta->table);
+
+ return [] unless @$links;
+
my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]);
my @objects = map { $get_objects->($_) } @$link_objs;
our @EXPORT = qw(calculate_prices_and_taxes);
use Carp;
-use List::Util qw(sum min);
+use List::Util qw(sum min max);
sub calculate_prices_and_taxes {
my ($self, %params) = @_;
+ require SL::DB::Chart;
+ require SL::DB::Currency;
+ require SL::DB::Default;
+ require SL::DB::InvoiceItem;
+ require SL::DB::Part;
require SL::DB::PriceFactor;
require SL::DB::Unit;
+ SL::DB::Part->load_cached(map { $_->parts_id } @{ $self->items });
+
my %units_by_name = map { ( $_->name => $_ ) } @{ SL::DB::Manager::Unit->get_all };
my %price_factors_by_id = map { ( $_->id => $_ ) } @{ SL::DB::Manager::PriceFactor->get_all };
$self->netamount( 0);
$self->marge_total(0);
+ SL::DB::Manager::Chart->cache_taxkeys(date => $self->transdate);
+
my $idx = 0;
foreach my $item ($self->items) {
$idx++;
sub _get_exchangerate {
my ($self, $data, %params) = @_;
- require SL::DB::Default;
- my $currency = $self->currency_id ? $self->currency->name || '' : '';
+ my $currency = $self->currency_id ? SL::DB::Currency->load_cached($self->currency_id)->name || '' : '';
if ($currency ne SL::DB::Default->get_default_currency) {
$data->{exchangerate} = $::form->check_exchangerate(\%::myconfig, $currency, $self->transdate, $data->{is_sales} ? 'buy' : 'sell');
$data->{exchangerate} ||= $params{exchangerate};
sub _calculate_item {
my ($self, $item, $idx, $data, %params) = @_;
- my $part_unit = $data->{units_by_name}->{ $item->part->unit };
- my $item_unit = $data->{units_by_name}->{ $item->unit };
+ my $part = SL::DB::Part->load_cached($item->parts_id);
+ my $part_unit = $data->{units_by_name}->{ $part->unit };
+ my $item_unit = $data->{units_by_name}->{ $item->unit };
- croak("Undefined unit " . $item->part->unit) if !$part_unit;
+ croak("Undefined unit " . $part->unit) if !$part_unit;
croak("Undefined unit " . $item->unit) if !$item_unit;
$item->base_qty($item_unit->convert_to($item->qty, $part_unit));
$item->fxsellprice($item->sellprice) if $data->{is_invoice};
- my $num_dec = _num_decimal_places($item->sellprice) || 2;
- # ^ we need at least 2 decimal places ^
- # my test case 43.00 € with 0 decimal places and 0.5 discount ->
- # : sellprice before:43.00000
- # : num dec before:0
- # : discount / sellprice ratio: 22 / 21
- # : discount = 43 * 0.5 _round(21.5, 0) = 22
- # TODO write a test case
+ my $num_dec = max 2, _num_decimal_places($item->sellprice);
my $discount = _round($item->sellprice * ($item->discount || 0), $num_dec);
my $sellprice = _round($item->sellprice - $discount, $num_dec);
$item->price_factor( ! $item->price_factor_obj ? 1 : ($item->price_factor_obj->factor || 1));
- $item->marge_price_factor(! $item->part->price_factor ? 1 : ($item->part->price_factor->factor || 1));
+ $item->marge_price_factor(! $part->price_factor ? 1 : ($part->price_factor->factor || 1));
my $linetotal = _round($sellprice * $item->qty / $item->price_factor, 2) * $data->{exchangerate};
$linetotal = _round($linetotal, 2);
$item->marge_percent(0);
} else {
- my $lastcost = ! ($item->lastcost * 1) ? ($item->part->lastcost || 0) : $item->lastcost;
+ my $lastcost = ! ($item->lastcost * 1) ? ($part->lastcost || 0) : $item->lastcost;
my $linetotal_cost = _round($lastcost * $item->qty / $item->marge_price_factor, 2);
$item->marge_total( $linetotal - $linetotal_cost);
$data->{lastcost_total} += $linetotal_cost;
}
- my $taxkey = $item->part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
+ my $taxkey = $part->get_taxkey(date => $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
my $tax_rate = $taxkey->tax->rate;
my $tax_amount = undef;
$self->netamount($self->netamount + $sellprice * $item->qty / $item->price_factor);
- my $chart = $item->part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id);
+ my $chart = $part->get_chart(type => $data->{is_sales} ? 'income' : 'expense', taxzone => $self->taxzone_id);
$data->{amounts}->{ $chart->id } ||= { taxkey => $taxkey->taxkey_id, tax_id => $taxkey->tax_id, amount => 0 };
$data->{amounts}->{ $chart->id }->{amount} += $linetotal;
$data->{amounts}->{ $chart->id }->{amount} -= $tax_amount if $self->taxincluded;
push @{ $data->{assembly_items} }, [];
- if ($item->part->is_assembly) {
- _calculate_assembly_item($self, $data, $item->part, $item->base_qty, $item->unit_obj->convert_to(1, $item->part->unit_obj));
- } elsif ($item->part->is_part) {
+ if ($part->is_assembly) {
+ _calculate_assembly_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit));
+ } elsif ($part->is_part) {
if ($data->{is_invoice}) {
- $item->allocated(_calculate_part_item($self, $data, $item->part, $item->base_qty, $item->unit_obj->convert_to(1, $item->part->unit_obj)));
+ $item->allocated(_calculate_part_item($self, $data, $part, $item->base_qty, $item_unit->convert_to(1, $part_unit)));
}
}
goto &transdate;
}
+sub transactions {
+ my ($self) = @_;
+
+ return unless $self->id;
+
+ require SL::DB::AccTransaction;
+ SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
+}
+
1;
__END__
use base qw(SL::DB::Helper::Manager);
use SL::DB::Helper::Sorted;
+use DateTime;
+use SL::DBUtils;
sub object_class { 'SL::DB::Chart' }
link => { like => "\%:${link}:\%" } ]);
}
+sub cache_taxkeys {
+ my ($self, %params) = @_;
+
+ my $date = $params{date} || DateTime->today;
+ my $cache = $::request->cache('::SL::DB::Chart::get_active_taxkey')->{$date} //= {};
+
+ require SL::DB::TaxKey;
+ my $tks = SL::DB::Manager::TaxKey->get_all;
+ my %tks_by_id = map { $_->id => $_ } @$tks;
+
+ my $rows = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $date);
+ SELECT DISTINCT ON (chart_id) chart_id, startdate, id
+ FROM taxkeys
+ WHERE startdate < ?
+ ORDER BY chart_id, startdate DESC;
+
+ for (@$rows) {
+ $cache->{$_->{chart_id}} = $tks_by_id{$_->{id}};
+ }
+}
+
1;
__END__
},
status => sub {
my ($key, $value, $prefix) = @_;
- return () if $value eq 'all';
+ return () if $value ne 'orphaned';
return __PACKAGE__->is_not_used_filter($prefix);
},
);
cp_email => { type => 'text' },
cp_fax => { type => 'text' },
cp_gender => { type => 'character', length => 1 },
- cp_givenname => { type => 'varchar', length => 75 },
+ cp_givenname => { type => 'text' },
cp_id => { type => 'integer', not_null => 1, sequence => 'id' },
cp_mobile1 => { type => 'text' },
cp_mobile2 => { type => 'text' },
- cp_name => { type => 'varchar', length => 75 },
- cp_phone1 => { type => 'varchar', length => 75 },
- cp_phone2 => { type => 'varchar', length => 75 },
- cp_position => { type => 'varchar', length => 75 },
+ cp_name => { type => 'text' },
+ cp_phone1 => { type => 'text' },
+ cp_phone2 => { type => 'text' },
+ cp_position => { type => 'text' },
cp_privatemail => { type => 'text' },
cp_privatphone => { type => 'text' },
cp_project => { type => 'text' },
cp_satfax => { type => 'text' },
cp_satphone => { type => 'text' },
cp_street => { type => 'text' },
- cp_title => { type => 'varchar', length => 75 },
+ cp_title => { type => 'text' },
cp_zipcode => { type => 'text' },
itime => { type => 'timestamp', default => 'now()' },
mtime => { type => 'timestamp' },
bank => { type => 'text' },
bank_code => { type => 'text' },
bcc => { type => 'text' },
- bic => { type => 'varchar', length => 100 },
+ bic => { type => 'text' },
business_id => { type => 'integer' },
c_vendor_id => { type => 'text' },
cc => { type => 'text' },
- city => { type => 'varchar', length => 75 },
+ city => { type => 'text' },
contact => { type => 'text' },
- country => { type => 'varchar', length => 75 },
+ country => { type => 'text' },
creditlimit => { type => 'numeric', default => '0', precision => 15, scale => 5 },
currency_id => { type => 'integer', not_null => 1 },
customernumber => { type => 'text' },
delivery_term_id => { type => 'integer' },
- department_1 => { type => 'varchar', length => 75 },
- department_2 => { type => 'varchar', length => 75 },
+ department_1 => { type => 'text' },
+ department_2 => { type => 'text' },
depositor => { type => 'text' },
direct_debit => { type => 'boolean', default => 'false' },
discount => { type => 'float', scale => 4 },
email => { type => 'text' },
- fax => { type => 'varchar', length => 30 },
+ fax => { type => 'text' },
greeting => { type => 'text' },
homepage => { type => 'text' },
hourly_rate => { type => 'numeric', precision => 8, scale => 2 },
- iban => { type => 'varchar', length => 100 },
+ iban => { type => 'text' },
id => { type => 'integer', not_null => 1, sequence => 'id' },
itime => { type => 'timestamp', default => 'now()' },
klass => { type => 'integer', default => '0' },
- language => { type => 'varchar', length => 5 },
+ language => { type => 'text' },
language_id => { type => 'integer' },
mandate_date_of_signature => { type => 'date' },
mandator_id => { type => 'text' },
payment_id => { type => 'integer' },
phone => { type => 'text' },
salesman_id => { type => 'integer' },
- street => { type => 'varchar', length => 75 },
+ street => { type => 'text' },
taxincluded => { type => 'boolean' },
taxincluded_checked => { type => 'boolean' },
taxnumber => { type => 'text' },
taxzone_id => { type => 'integer', default => '0', not_null => 1 },
terms => { type => 'integer', default => '0' },
user_password => { type => 'text' },
- username => { type => 'varchar', length => 50 },
+ username => { type => 'text' },
ustid => { type => 'text' },
- zipcode => { type => 'varchar', length => 10 },
+ zipcode => { type => 'text' },
);
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
__PACKAGE__->meta->table('defaults');
__PACKAGE__->meta->columns(
- accounting_method => { type => 'text' },
- address => { type => 'text' },
- ap_changeable => { type => 'integer', default => 2, not_null => 1 },
- ap_show_mark_as_paid => { type => 'boolean', default => 'true' },
- ar_changeable => { type => 'integer', default => 2, not_null => 1 },
- ar_paid_accno_id => { type => 'integer' },
- ar_show_mark_as_paid => { type => 'boolean', default => 'true' },
- articlenumber => { type => 'text' },
- assemblynumber => { type => 'text' },
- balance_startdate_method => { type => 'text' },
- bin_id => { type => 'integer' },
- bin_id_ignore_onhand => { type => 'integer' },
- businessnumber => { type => 'text' },
- closedto => { type => 'date' },
- cnnumber => { type => 'text' },
- co_ustid => { type => 'text' },
- coa => { type => 'text' },
- company => { type => 'text' },
- currency_id => { type => 'integer', not_null => 1 },
- customer_hourly_rate => { type => 'numeric', precision => 8, scale => 2 },
- customernumber => { type => 'text' },
- datev_check_on_ap_transaction => { type => 'boolean', default => 'true' },
- datev_check_on_ar_transaction => { type => 'boolean', default => 'true' },
- datev_check_on_gl_transaction => { type => 'boolean', default => 'true' },
- datev_check_on_purchase_invoice => { type => 'boolean', default => 'true' },
- datev_check_on_sales_invoice => { type => 'boolean', default => 'true' },
- dunning_ar => { type => 'integer' },
- dunning_ar_amount_fee => { type => 'integer' },
- dunning_ar_amount_interest => { type => 'integer' },
- duns => { type => 'text' },
- expense_accno_id => { type => 'integer' },
- fxgain_accno_id => { type => 'integer' },
- fxloss_accno_id => { type => 'integer' },
- gl_changeable => { type => 'integer', default => 2, not_null => 1 },
- id => { type => 'serial', not_null => 1 },
- income_accno_id => { type => 'integer' },
- inventory_accno_id => { type => 'integer' },
- inventory_system => { type => 'text' },
- invnumber => { type => 'text' },
- ir_changeable => { type => 'integer', default => 2, not_null => 1 },
- ir_show_mark_as_paid => { type => 'boolean', default => 'true' },
- is_changeable => { type => 'integer', default => 2, not_null => 1 },
- is_show_mark_as_paid => { type => 'boolean', default => 'true' },
- itime => { type => 'timestamp', default => 'now()' },
- language_id => { type => 'integer' },
- max_future_booking_interval => { type => 'integer', default => 360 },
- mtime => { type => 'timestamp' },
- normalize_part_descriptions => { type => 'boolean', default => 'true' },
- normalize_vc_names => { type => 'boolean', default => 'true' },
- parts_image_css => { type => 'text', default => 'border:0;float:left;max-width:250px;margin-top:20px:margin-right:10px;margin-left:10px;' },
- parts_listing_image => { type => 'boolean', default => 'true' },
- parts_show_image => { type => 'boolean', default => 'true' },
- payments_changeable => { type => 'integer', default => '0', not_null => 1 },
- pdonumber => { type => 'text' },
- ponumber => { type => 'text' },
- profit_determination => { type => 'text' },
- purchase_delivery_order_show_delete => { type => 'boolean', default => 'true' },
- purchase_order_show_delete => { type => 'boolean', default => 'true' },
- requirement_spec_section_order_part_id => { type => 'integer' },
- revtrans => { type => 'boolean', default => 'false' },
- rfqnumber => { type => 'text' },
- rmanumber => { type => 'text' },
- sales_delivery_order_show_delete => { type => 'boolean', default => 'true' },
- sales_order_show_delete => { type => 'boolean', default => 'true' },
- sdonumber => { type => 'text' },
- sepa_creditor_id => { type => 'text' },
- servicenumber => { type => 'text' },
- show_bestbefore => { type => 'boolean', default => 'false' },
- show_weight => { type => 'boolean', default => 'false', not_null => 1 },
- signature => { type => 'text' },
- sonumber => { type => 'text' },
- sqnumber => { type => 'text' },
- taxnumber => { type => 'text' },
- templates => { type => 'text' },
- transfer_default => { type => 'boolean', default => 'true' },
- transfer_default_ignore_onhand => { type => 'boolean', default => 'false' },
- transfer_default_use_master_default_bin => { type => 'boolean', default => 'false' },
- vendornumber => { type => 'text' },
- version => { type => 'varchar', length => 8 },
- vertreter => { type => 'boolean', default => 'false' },
- warehouse_id => { type => 'integer' },
- warehouse_id_ignore_onhand => { type => 'integer' },
- webdav => { type => 'boolean', default => 'false' },
- webdav_documents => { type => 'boolean', default => 'false' },
- weightunit => { type => 'varchar', length => 5 },
+ accounting_method => { type => 'text' },
+ address => { type => 'text' },
+ allow_new_purchase_delivery_order => { type => 'boolean', default => 'true', not_null => 1 },
+ allow_new_purchase_invoice => { type => 'boolean', default => 'true', not_null => 1 },
+ allow_sales_invoice_from_sales_order => { type => 'boolean', default => 'true', not_null => 1 },
+ allow_sales_invoice_from_sales_quotation => { type => 'boolean', default => 'true', not_null => 1 },
+ ap_changeable => { type => 'integer', default => 2, not_null => 1 },
+ ap_show_mark_as_paid => { type => 'boolean', default => 'true' },
+ ar_changeable => { type => 'integer', default => 2, not_null => 1 },
+ ar_paid_accno_id => { type => 'integer' },
+ ar_show_mark_as_paid => { type => 'boolean', default => 'true' },
+ articlenumber => { type => 'text' },
+ assemblynumber => { type => 'text' },
+ balance_startdate_method => { type => 'text' },
+ bin_id => { type => 'integer' },
+ bin_id_ignore_onhand => { type => 'integer' },
+ businessnumber => { type => 'text' },
+ closedto => { type => 'date' },
+ cnnumber => { type => 'text' },
+ co_ustid => { type => 'text' },
+ coa => { type => 'text' },
+ company => { type => 'text' },
+ currency_id => { type => 'integer', not_null => 1 },
+ customer_hourly_rate => { type => 'numeric', precision => 8, scale => 2 },
+ customer_projects_only_in_sales => { type => 'boolean', default => 'false', not_null => 1 },
+ customernumber => { type => 'text' },
+ datev_check_on_ap_transaction => { type => 'boolean', default => 'true' },
+ datev_check_on_ar_transaction => { type => 'boolean', default => 'true' },
+ datev_check_on_gl_transaction => { type => 'boolean', default => 'true' },
+ datev_check_on_purchase_invoice => { type => 'boolean', default => 'true' },
+ datev_check_on_sales_invoice => { type => 'boolean', default => 'true' },
+ dunning_ar => { type => 'integer' },
+ dunning_ar_amount_fee => { type => 'integer' },
+ dunning_ar_amount_interest => { type => 'integer' },
+ duns => { type => 'text' },
+ expense_accno_id => { type => 'integer' },
+ fxgain_accno_id => { type => 'integer' },
+ fxloss_accno_id => { type => 'integer' },
+ gl_changeable => { type => 'integer', default => 2, not_null => 1 },
+ id => { type => 'serial', not_null => 1 },
+ income_accno_id => { type => 'integer' },
+ inventory_accno_id => { type => 'integer' },
+ inventory_system => { type => 'text' },
+ invnumber => { type => 'text' },
+ ir_changeable => { type => 'integer', default => 2, not_null => 1 },
+ ir_show_mark_as_paid => { type => 'boolean', default => 'true' },
+ is_changeable => { type => 'integer', default => 2, not_null => 1 },
+ is_show_mark_as_paid => { type => 'boolean', default => 'true' },
+ itime => { type => 'timestamp', default => 'now()' },
+ language_id => { type => 'integer' },
+ max_future_booking_interval => { type => 'integer', default => 360 },
+ mtime => { type => 'timestamp' },
+ normalize_part_descriptions => { type => 'boolean', default => 'true' },
+ normalize_vc_names => { type => 'boolean', default => 'true' },
+ parts_image_css => { type => 'text', default => 'border:0;float:left;max-width:250px;margin-top:20px:margin-right:10px;margin-left:10px;' },
+ parts_listing_image => { type => 'boolean', default => 'true' },
+ parts_show_image => { type => 'boolean', default => 'true' },
+ payments_changeable => { type => 'integer', default => '0', not_null => 1 },
+ pdonumber => { type => 'text' },
+ ponumber => { type => 'text' },
+ profit_determination => { type => 'text' },
+ purchase_delivery_order_show_delete => { type => 'boolean', default => 'true' },
+ purchase_order_show_delete => { type => 'boolean', default => 'true' },
+ require_transaction_description_ps => { type => 'boolean', default => 'false', not_null => 1 },
+ requirement_spec_section_order_part_id => { type => 'integer' },
+ revtrans => { type => 'boolean', default => 'false' },
+ rfqnumber => { type => 'text' },
+ rmanumber => { type => 'text' },
+ sales_delivery_order_show_delete => { type => 'boolean', default => 'true' },
+ sales_order_show_delete => { type => 'boolean', default => 'true' },
+ sdonumber => { type => 'text' },
+ sepa_creditor_id => { type => 'text' },
+ servicenumber => { type => 'text' },
+ show_bestbefore => { type => 'boolean', default => 'false' },
+ show_weight => { type => 'boolean', default => 'false', not_null => 1 },
+ signature => { type => 'text' },
+ sonumber => { type => 'text' },
+ sqnumber => { type => 'text' },
+ taxnumber => { type => 'text' },
+ templates => { type => 'text' },
+ transfer_default => { type => 'boolean', default => 'true' },
+ transfer_default_ignore_onhand => { type => 'boolean', default => 'false' },
+ transfer_default_use_master_default_bin => { type => 'boolean', default => 'false' },
+ vendornumber => { type => 'text' },
+ version => { type => 'varchar', length => 8 },
+ vertreter => { type => 'boolean', default => 'false' },
+ warehouse_id => { type => 'integer' },
+ warehouse_id_ignore_onhand => { type => 'integer' },
+ webdav => { type => 'boolean', default => 'false' },
+ webdav_documents => { type => 'boolean', default => 'false' },
+ weightunit => { type => 'varchar', length => 5 },
);
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
__PACKAGE__->meta->allow_inline_column_values(1);
__PACKAGE__->meta->foreign_keys(
- created_for => {
+ created_by => {
class => 'SL::DB::Employee',
- key_columns => { created_for_user => 'id' },
+ key_columns => { created_by => 'id' },
},
- employee => {
+ created_for => {
class => 'SL::DB::Employee',
- key_columns => { created_by => 'id' },
+ key_columns => { created_for_user => 'id' },
},
note => {
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
__PACKAGE__->meta->foreign_keys(
- employee => {
+ to_follow_ups_by => {
class => 'SL::DB::Employee',
key_columns => { what => 'id' },
},
- employee_obj => {
+ with_access => {
class => 'SL::DB::Employee',
key_columns => { who => 'id' },
},
delivery_vendor_id => { type => 'integer' },
department_id => { type => 'integer' },
employee_id => { type => 'integer' },
+ expected_billing_date => { type => 'date' },
globalproject_id => { type => 'integer' },
id => { type => 'integer', not_null => 1, sequence => 'id' },
intnotes => { type => 'text' },
mtime => { type => 'timestamp' },
netamount => { type => 'numeric', precision => 15, scale => 5 },
notes => { type => 'text' },
+ order_probability => { type => 'integer', default => '0', not_null => 1 },
ordnumber => { type => 'text', not_null => 1 },
payment_id => { type => 'integer' },
proforma => { type => 'boolean', default => 'false' },
module => { type => 'text' },
mtime => { type => 'timestamp' },
shipto_id => { type => 'integer', not_null => 1, sequence => 'id' },
- shiptocity => { type => 'varchar', length => 75 },
- shiptocontact => { type => 'varchar', length => 75 },
- shiptocountry => { type => 'varchar', length => 75 },
+ shiptocity => { type => 'text' },
+ shiptocontact => { type => 'text' },
+ shiptocountry => { type => 'text' },
shiptocp_gender => { type => 'text' },
- shiptodepartment_1 => { type => 'varchar', length => 75 },
- shiptodepartment_2 => { type => 'varchar', length => 75 },
+ shiptodepartment_1 => { type => 'text' },
+ shiptodepartment_2 => { type => 'text' },
shiptoemail => { type => 'text' },
- shiptofax => { type => 'varchar', length => 30 },
- shiptoname => { type => 'varchar', length => 75 },
- shiptophone => { type => 'varchar', length => 30 },
- shiptostreet => { type => 'varchar', length => 75 },
- shiptozipcode => { type => 'varchar', length => 75 },
+ shiptofax => { type => 'text' },
+ shiptoname => { type => 'text' },
+ shiptophone => { type => 'text' },
+ shiptostreet => { type => 'text' },
+ shiptozipcode => { type => 'text' },
trans_id => { type => 'integer' },
);
bank => { type => 'text' },
bank_code => { type => 'text' },
bcc => { type => 'text' },
- bic => { type => 'varchar', length => 100 },
+ bic => { type => 'text' },
business_id => { type => 'integer' },
cc => { type => 'text' },
- city => { type => 'varchar', length => 75 },
+ city => { type => 'text' },
contact => { type => 'text' },
- country => { type => 'varchar', length => 75 },
+ country => { type => 'text' },
creditlimit => { type => 'numeric', precision => 15, scale => 5 },
currency_id => { type => 'integer', not_null => 1 },
delivery_term_id => { type => 'integer' },
- department_1 => { type => 'varchar', length => 75 },
- department_2 => { type => 'varchar', length => 75 },
+ department_1 => { type => 'text' },
+ department_2 => { type => 'text' },
depositor => { type => 'text' },
direct_debit => { type => 'boolean', default => 'false' },
discount => { type => 'float', scale => 4 },
email => { type => 'text' },
- fax => { type => 'varchar', length => 30 },
+ fax => { type => 'text' },
greeting => { type => 'text' },
homepage => { type => 'text' },
- iban => { type => 'varchar', length => 100 },
+ iban => { type => 'text' },
id => { type => 'integer', not_null => 1, sequence => 'id' },
itime => { type => 'timestamp', default => 'now()' },
- language => { type => 'varchar', length => 5 },
+ language => { type => 'text' },
language_id => { type => 'integer' },
mtime => { type => 'timestamp' },
name => { type => 'text', not_null => 1 },
payment_id => { type => 'integer' },
phone => { type => 'text' },
salesman_id => { type => 'integer' },
- street => { type => 'varchar', length => 75 },
+ street => { type => 'text' },
taxincluded => { type => 'boolean' },
taxnumber => { type => 'text' },
taxzone_id => { type => 'integer', default => '0', not_null => 1 },
terms => { type => 'integer', default => '0' },
- user_password => { type => 'varchar', length => 12 },
- username => { type => 'varchar', length => 50 },
+ user_password => { type => 'text' },
+ username => { type => 'text' },
ustid => { type => 'text' },
v_customer_id => { type => 'text' },
vendornumber => { type => 'text' },
- zipcode => { type => 'varchar', length => 10 },
+ zipcode => { type => 'text' },
);
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
use strict;
+use Carp;
use English qw(-no_match_vars);
use Rose::DB::Object;
use List::MoreUtils qw(any);
return $result;
}
+sub load_cached {
+ my $class_or_self = shift;
+ my @ids = @_;
+ my $class = ref($class_or_self) || $class_or_self;
+ my $cache = $::request->cache("::SL::DB::Object::object_cache::${class}");
+
+ croak "Missing ID" unless @ids;
+
+ my @missing_ids = grep { !exists $cache->{$_} } @ids;
+
+ return $cache->{$ids[0]} if !@missing_ids;
+
+ croak "Caching can only be used with classes with exactly one primary key column" if 1 != scalar(@{ $class->meta->primary_key_columns });
+
+ my $primary_key = $class->meta->primary_key_columns->[0]->name;
+ my $objects = $class->_get_manager_class->get_all(where => [ $primary_key => \@missing_ids ]);
+
+ $cache->{$_->$primary_key} = $_ for @{ $objects};
+
+ return $cache->{$ids[0]};
+}
+
+sub invalidate_cached {
+ my ($class_or_self, @ids) = @_;
+ my $class = ref($class_or_self) || $class_or_self;
+
+ if (ref($class_or_self) && !@ids) {
+ croak "Caching can only be used with classes with exactly one primary key column" if 1 != scalar(@{ $class->meta->primary_key_columns });
+
+ my $primary_key = $class->meta->primary_key_columns->[0]->name;
+ @ids = ($class_or_self->$primary_key);
+ }
+
+ delete @{ $::request->cache("::SL::DB::Object::object_cache::${class}") }{ @ids };
+
+ return $class_or_self;
+}
+
1;
__END__
=pod
+=encoding utf8
+
=head1 NAME
SL::DB::Object: Base class for all of our model classes
be used to check whether or not an object's columns are unique before
saving or during validation.
+=item C<load_cached @ids>
+
+Loads objects from the database which haven't been cached before and
+caches them for the duration of the current request (see
+L<SL::Request/cache>).
+
+This method can be called both as an instance method and a class
+method. It loads objects for the corresponding class (e.g. both
+C<SL::DB::Part-E<gt>load_cached(…)> and
+C<$some_part-E<gt>load_cached(…)> will load parts).
+
+Currently only classes with a single primary key column are supported.
+
+Returns the cached object for the first ID.
+
+=item C<invalidate_cached @ids>
+
+Deletes all cached instances of this class (see L</load_cached>) for
+the given IDs.
+
+If called as an instance method without further arguments then the
+object's ID is used.
+
+Returns the object/class it was called on.
+
=back
=head1 AUTHOR
my $date = $params{date} || DateTime->today_local;
my $is_sales = !!$params{is_sales};
my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
+ my $tk_info = $::request->cache('get_taxkey');
- $self->{__partpriv_taxkey_information} ||= { };
- my $tk_info = $self->{__partpriv_taxkey_information};
+ $tk_info->{$self->id} //= {};
+ $tk_info->{$self->id}->{$taxzone} //= { };
+ my $cache = $tk_info->{$self->id}->{$taxzone}->{$is_sales} //= { };
- $tk_info->{$taxzone} ||= { };
- $tk_info->{$taxzone}->{$is_sales} ||= { };
-
- if (!exists $tk_info->{$taxzone}->{$is_sales}->{$date}) {
- $tk_info->{$taxzone}->{$is_sales}->{$date} =
+ if (!exists $cache->{$date}) {
+ $cache->{$date} =
$self->get_chart(type => $is_sales ? 'income' : 'expense', taxzone => $taxzone)
- ->load
->get_active_taxkey($date);
}
- return $tk_info->{$taxzone}->{$is_sales}->{$date};
+ return $cache->{$date};
}
sub get_chart {
my $type = (any { $_ eq $params{type} } qw(income expense inventory)) ? $params{type} : croak("Invalid 'type' parameter '$params{type}'");
my $taxzone = $params{ defined($params{taxzone}) ? 'taxzone' : 'taxzone_id' } * 1;
- $self->{__partpriv_get_chart_id} ||= { };
- my $charts = $self->{__partpriv_get_chart_id};
+ my $charts = $::request->cache('get_chart_id/by_part_id_and_taxzone')->{$self->id} //= {};
+ my $all_charts = $::request->cache('get_chart_id/by_id');
$charts->{$taxzone} ||= { };
if (!exists $charts->{$taxzone}->{$type}) {
- my $bugru = $self->buchungsgruppe;
+ require SL::DB::Buchungsgruppe;
+ my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
: $bugru->call_sub("${type}_accno_id_${taxzone}");
- $charts->{$taxzone}->{$type} = $chart_id ? SL::DB::Chart->new(id => $chart_id)->load : undef;
+ if ($chart_id) {
+ my $chart = $all_charts->{$chart_id} // SL::DB::Chart->load_cached($chart_id)->load;
+ $all_charts->{$chart_id} = $chart;
+ $charts->{$taxzone}->{$type} = $chart;
+ }
}
return $charts->{$taxzone}->{$type};
sub base_factor {
my ($self) = @_;
- if (!defined $self->{__base_factor}) {
- $self->{__base_factor} = !$self->base_unit || !$self->factor || ($self->name eq $self->base_unit) ? 1 : $self->factor * $self->base->base_factor;
+ my $cache = $::request->cache('base_factor');
+
+ if (!defined $cache->{$self->id}) {
+ $cache->{$self->id} = !$self->base_unit || !$self->factor || ($self->name eq $self->base_unit) ? 1 : $self->factor * $self->base->base_factor;
}
- return $self->{__base_factor};
+ return $cache->{$self->id};
}
sub convert_to {
croak "File '${src_dir}/$_' does not exist" unless -f "${src_dir}/$_";
}
- my $template_dir = $::instance_conf->reload->get_templates;
+ return 1 unless my $template_dir = $::instance_conf->reload->get_templates;
$::lxdebug->message(LXDebug::DEBUG1(), "add_print_templates: template_dir $template_dir");
- return 1 if !$template_dir;
-
foreach my $src_file (@files) {
my $dest_file = $template_dir . '/' . $src_file;
conv_i($form->{id}));
do_query($form, $dbh, $query, @values);
- # add shipto
$form->{name} = $form->{ $form->{vc} };
$form->{name} =~ s/--$form->{"$form->{vc}_id"}//;
+ # add shipto
if (!$form->{shipto_id}) {
$form->add_shipto($dbh, $form->{id}, "DO");
}
die "cannot find locale for user " . $params{login} unless $::locale = Locale->new($::myconfig{countrycode});
$::form->{login} = $params{login}; # normaly implicit at login
-
- $::instance_conf->init;
}
}
$::locale = Locale->new($::myconfig{countrycode});
$::form->{error} = $::locale->text('The session is invalid or has expired.') if ($error_type eq 'session');
$::form->{error} = $::locale->text('Incorrect password!') if ($error_type eq 'password');
+ $::form->{error} = $::locale->text('The action is missing or invalid.') if ($error_type eq 'action');
return render_error_ajax($::form->{error}) if $::request->is_ajax;
$::form->read_cgi_input;
my %routing;
- eval { %routing = _route_request($ENV{SCRIPT_NAME}); 1; } or return;
+ eval { %routing = $self->_route_request($ENV{SCRIPT_NAME}); 1; } or return;
($routing_type, $script_name, $action) = @routing{qw(type controller action)};
$::lxdebug->log_request($routing_type, $script_name, $action);
if ( (($script eq 'login') && !$action)
|| ($script eq 'admin')
|| (SL::Auth::SESSION_EXPIRED() == $session_result)) {
- $self->redirect_to_login($script);
+ $self->redirect_to_login(script => $script, error => 'session');
}
}
sub redirect_to_login {
- my ($self, $script) = @_;
- my $action = $script =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login&error=session';
+ my ($self, %params) = @_;
+ my $action = ($params{script} // '') =~ m/^admin/i ? 'Admin/login' : 'LoginScreen/user_login';
+ $action .= '&error=' . $params{error} if $params{error};
+
print $::request->cgi->redirect("controller.pl?action=${action}");
::end_of_request();
}
}
sub _route_request {
- my $script_name = shift;
+ my ($self, $script_name) = @_;
- return $script_name =~ m/dispatcher\.pl$/ ? (type => 'old', _route_dispatcher_request())
- : $script_name =~ m/controller\.pl/ ? (type => 'controller', _route_controller_request())
+ return $script_name =~ m/dispatcher\.pl$/ ? (type => 'old', $self->_route_dispatcher_request)
+ : $script_name =~ m/controller\.pl/ ? (type => 'controller', $self->_route_controller_request)
: (type => 'old', controller => $script_name, action => $::form->{action});
}
sub _route_dispatcher_request {
+ my ($self) = @_;
my $name_re = qr{[a-z]\w*};
my ($script_name, $action);
}
sub _route_controller_request {
+ my ($self) = @_;
my ($controller, $action, $request_type);
eval {
+ # Redirect simple requests to controller.pl without any GET/POST
+ # param to the login page.
+ $self->redirect_to_login(error => 'action') if !$::form->{action};
+
+ # Show an error if the »action« parameter doesn't match the
+ # pattern »Controller/action«.
$::form->{action} =~ m|^ ( [A-Z] [A-Za-z0-9_]* ) / ( [a-z] [a-z0-9_]* ) ( \. [a-zA-Z]+ )? $|x || die "Unroutable request -- invalid controller/action.\n";
($controller, $action) = ($1, $2);
delete $::form->{action};
$::auth->create_or_refresh_session;
$::auth->delete_session_value('FLASH');
+ $::instance_conf->reload->data;
return 1;
}
use Carp;
use Data::Dumper;
+use Carp;
use CGI;
use Cwd;
use Encode;
$self->show_generic_error($msg);
} else {
- print STDERR "Error: $msg\n";
- ::end_of_request();
+ confess "Error: $msg\n";
}
$main::lxdebug->leave_sub();
if (-f "templates/webpages/${file}.html") {
$file = "templates/webpages/${file}.html";
+ } elsif (ref $file eq 'SCALAR') {
+ # file is a scalarref, use inline mode
} else {
my $info = "Web page template '${file}' not found.\n";
+ $::form->header;
print qq|<pre>$info</pre>|;
::end_of_request();
}
my $dbh = $self->get_standard_dbh(\%main::myconfig);
my ($sth, $query, $ref);
- my $vc = $self->{"vc"} eq "customer" ? "customer" : "vendor";
- my $vc_id = $self->{"${vc}_id"};
+ my ($vc, $vc_id);
+ if ($params{contacts} || $params{shipto}) {
+ $vc = 'customer' if $self->{"vc"} eq "customer";
+ $vc = 'vendor' if $self->{"vc"} eq "vendor";
+ die "invalid use of get_lists, need 'vc'";
+ $vc_id = $self->{"${vc}_id"};
+ }
if ($params{"contacts"}) {
$self->_get_contacts($dbh, $vc_id, $params{"contacts"});
$self->{"employee_${_}"} = $defaults->$_ for qw(address businessnumber co_ustid company duns sepa_creditor_id taxnumber);
}
- # set shipto from billto unless set
- my $has_shipto = any { $self->{"shipto$_"} } qw(name street zipcode city country contact);
- if (!$has_shipto && ($self->{type} =~ m/^(?:purchase_order|request_quotation)$/)) {
- $self->{shiptoname} = $defaults->company;
- $self->{shiptostreet} = $defaults->address;
+ # Load shipping address from database if shipto_id is set.
+ if ($self->{shipto_id}) {
+ my $shipto = SL::DB::Shipto->new(id => $self->{shipto_id})->load;
+ $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns };
}
my $language = $self->{language} ? '_' . $self->{language} : '';
my ($class, %params) = @_;
my $userspath = $::lx_office_conf{paths}->{userspath};
+ my $vars = $params{variables} || {};
my $form = Form->new('');
+ $form->{$_} = $vars->{$_} for keys %{ $vars };
$form->{format} = 'pdf';
$form->{cwd} = getcwd();
$form->{templates} = $::instance_conf->get_templates;
$form->{tmpdir} = $form->{cwd} . '/' . $userspath;
my ($suffix) = $params{template} =~ m{\.(.+)};
- my $vars = $params{variables} || {};
- $form->{$_} = $vars->{$_} for keys %{ $vars };
-
my $temp_fh;
($temp_fh, $form->{tmpfile}) = File::Temp::tempfile(
'kivitendo-printXXXXXX',
goto &from_kivitendo;
}
+sub add_business_duration {
+ my ($self, %params) = @_;
+
+ my $abs_days = abs $params{days};
+ my $neg = $params{days} < 0;
+ my $bweek = $params{businessweek} || 5;
+ my $weeks = int ($abs_days / $bweek);
+ my $days = $abs_days % $bweek;
+
+ if ($neg) {
+ $self->subtract(weeks => $weeks);
+ $self->add(days => 8 - $self->day_of_week) if $self->day_of_week > $bweek;
+ $self->subtract(days => $self->day_of_week > $days ? $days : $days + (7 - $bweek));
+ } else {
+ $self->add(weeks => $weeks);
+ $self->subtract(days => $self->day_of_week - $bweek) if $self->day_of_week > $bweek;
+ $self->add(days => $self->day_of_week + $days <= $bweek ? $days : $days + (7 - $bweek));
+ }
+
+ $self;
+}
+
+sub add_businessdays {
+ my ($self, %params) = @_;
+
+ $self->add_business_duration(%params);
+}
+
+sub subtract_businessdays {
+ my ($self, %params) = @_;
+
+ $params{days} *= -1;
+
+ $self->add_business_duration(%params);
+}
+
1;
__END__
}
}
+ my $parts = SL::DB::Manager::Part->get_all(query => [ id => \@part_ids ]);
+ my %parts_by_id = map { $_->id => $_ } @$parts;
+
+ for my $i (1..$rowcount) {
+ my $id = $form->{"${prefix}${i}"};
+ next unless $id;
+
+ push @{ $form->{TEMPLATE_ARRAYS}{part_type} }, $parts_by_id{$id}->type;
+ }
+
$main::lxdebug->leave_sub();
}
}
- # add shipto
$form->{name} = $form->{vendor};
$form->{name} =~ s/--\Q$form->{vendor_id}\E//;
+
+ # add shipto
$form->add_shipto($dbh, $form->{id}, "AP");
# delete zero entries
}
$sth->finish();
- # get shipto if we do not convert an order or invoice
- if (!$params->{shipto}) {
- delete @{$params}{qw(shiptoname shiptostreet shiptozipcode shiptocity shiptocountry shiptocontact shiptophone shiptofax shiptoemail)};
-
- $query = qq|SELECT * FROM shipto WHERE (trans_id = ?) AND (module= 'CT')|;
- $ref = selectfirst_hashref_query($form, $dbh, $query, $vid);
- @{$params}{keys %$ref} = @{$ref}{keys %$ref};
- map { $params->{$_} = $ref->{$_} } keys %$ref;
- }
-
if (!$params->{id} && $params->{type} !~ /_(order|quotation)/) {
# setup last accounts used
$query =
do_query($form, $dbh, qq|UPDATE ar SET paid = amount WHERE id = ?|, conv_i($form->{"id"}));
}
- # add shipto
$form->{name} = $form->{customer};
$form->{name} =~ s/--\Q$form->{customer_id}\E//;
+ # add shipto
if (!$form->{shipto_id}) {
$form->add_shipto($dbh, $form->{id}, "AR");
}
$form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{invdate}, "buy");
- # get shipto
- $query = qq|SELECT * FROM shipto WHERE (trans_id = ?) AND (module = 'AR')|;
- $ref = selectfirst_hashref_query($form, $dbh, $query, $id);
- delete $ref->{id};
- map { $form->{$_} = $ref->{$_} } keys %{ $ref };
-
foreach my $vc (qw(customer vendor)) {
next if !$form->{"delivery_${vc}_id"};
($form->{"delivery_${vc}_string"}) = selectrow_query($form, $dbh, qq|SELECT name FROM customer WHERE id = ?|, $id);
}
$sth->finish;
- # get shipto if we did not converted an order or invoice
- if (!$form->{shipto}) {
- map { delete $form->{$_} }
- qw(shiptoname shiptodepartment_1 shiptodepartment_2
- shiptostreet shiptozipcode shiptocity shiptocountry
- shiptocontact shiptophone shiptofax shiptoemail);
-
- $query = qq|SELECT * FROM shipto WHERE trans_id = ? AND module = 'CT'|;
- $ref = selectfirst_hashref_query($form, $dbh, $query, $cid);
- delete $ref->{id};
- map { $form->{$_} = $ref->{$_} } keys %$ref;
- }
-
# setup last accounts used for this customer
if (!$form->{id} && $form->{type} !~ /_(order|quotation)/) {
$query =
sub _write {
no warnings;
my ($self, $prefix, $message) = @_;
- my $date = strftime("%Y-%m-%d %H:%M:%S $$ [" . getppid() . "] ${prefix}: ", localtime(time()));
+ my @now = gettimeofday();
+ my $date = strftime("%Y-%m-%d %H:%M:%S." . sprintf('%03d', int($now[1] / 1000)) . " $$ [" . getppid() . "] ${prefix}: ", localtime($now[0]));
local *FILE;
chomp($message);
--- /dev/null
+package SL::LiquidityProjection;
+
+use strict;
+
+use List::MoreUtils qw(uniq);
+
+use SL::DBUtils;
+
+sub new {
+ my $package = shift;
+ my $self = bless {}, $package;
+
+ my %params = @_;
+
+ $self->{params} = \%params;
+
+ my @now = localtime;
+ my $now_year = $now[5] + 1900;
+ my $now_month = $now[4] + 1;
+
+ $self->{min_date} = _the_date($now_year, $now_month);
+ $self->{max_date} = _the_date($now_year, $now_month + $params{months} - 1);
+
+ $self;
+}
+
+# Algorithmus:
+#
+# Für den aktuellen Monat und alle x Folgemonate soll der geplante
+# Liquiditätszufluss aufgeschlüsselt werden. Der Zufluss berechnet
+# sich dabei aus:
+#
+# 1. Summe aller offenen Auträge
+#
+# 2. abzüglich aller zu diesen Aufträgen erstellten Rechnungen
+# (Teillieferungen/Teilrechnungen)
+#
+# 3. zuzüglich alle aktiven Wartungsverträge, die in dem jeweiligen
+# Monat ihre Saldierungsperiode haben, außer Wartungsverträgen, die
+# für den jeweiligen Monat bereits abgerechnet wurden.
+#
+# Diese Werte sollen zusätzlich optional nach Verkäufer(in) und nach
+# Buchungsgruppe aufgeschlüsselt werden.
+#
+# Diese Lösung geht deshalb immer über die Positionen der Belege
+# (wegen der Buchungsgruppe) und berechnet die Summen daraus manuell.
+#
+# Alle Aufträge, deren Lieferdatum leer ist, oder deren Lieferdatum
+# vor dem aktuellen Monat liegt, werden in einer Kategorie 'alt'
+# zusammengefasst.
+#
+# Alle Aufträge, deren Lieferdatum nach dem zu betrachtenden Zeitraum
+# (aktueller Monat + x Monate) liegen, werden in einer Kategorie
+# 'Zukunft' zusammengefasst.
+#
+# Insgesamt läuft es wie folgt ab:
+#
+# 1. Es wird das Datum aller periodisch erzeugten Rechnungen innerhalb
+# des Betrachtungszeitraumes herausgesucht.
+#
+# 2. Alle aktiven Wartungsvertragskonfigurationen werden
+# ausgelesen. Die Saldierungsmonate werden solange aufaddiert, wie der
+# dabei herauskommende Monat nicht nach dem zu betrachtenden Zeitraum
+# liegt.
+#
+# 3. Für jedes Saldierungsintervall, das innerhalb des
+# Betrachtungszeitraumes liegt, und für das es für den Monat noch
+# keine Rechnung gibt (siehe 1.), wird diese Konfiguration für den
+# Monat vorgemerkt.
+#
+# 4. Es werden für alle offenen Kundenaufträge die Positionen
+# ausgelesen und mit Verkäufer(in), Buchungsgruppe verknüpft. Aus
+# Menge, Einzelpreis und Zeilenrabatt wird die Zeilensumme berechnet.
+#
+# 5. Mit den Informationen aus 3. und 4. werden Datenstrukturen
+# initialisiert, die für die Gesamtsummen, für alle Verkäufer(innen),
+# für alle Buchungsgruppen, für alle Monate Werte enthalten.
+#
+# 6. Es wird über alle Einträge aus 4. iteriert. Die Zeilensummen
+# werden in den entsprechenden Datenstrukturen aus 5. addiert.
+#
+# 7. Es wird über alle Einträge aus 3. iteriert. Die Zeilensummen
+# werden in den entsprechenden Datenstrukturen aus 5. addiert.
+#
+# 8. Es werden alle Rechnungspositionen ausgelesen, bei denen die
+# Auftragsnummer einer der aus 5. ermittelten Aufträge entspricht.
+#
+# 9. Es wird über alle Einträge aus 8. iteriert. Die Zeilensummen
+# werden von den entsprechenden Datenstrukturen aus 5. abgezogen. Als
+# Datum wird dabei das Datum des zu der Rechnung gehörenden Auftrages
+# genommen. Als Buchungsgruppe wird die Buchungsgruppe der Zeile
+# genommen. Falls es passieren sollte, dass diese Buchungsgruppe in
+# den Aufträgen nie vorgekommen ist (sprich Rechnung enthält
+# Positionen, die im Auftrag nicht enthalten sind, und die komplett
+# andere Buchungsgruppen verwenden), so wird schlicht die allererste
+# in 4. gefundene Buchungsgruppe damit belastet.
+
+sub create {
+ my ($self) = @_;
+ my %params = %{ $self->{params} };
+
+ my $dbh = $params{dbh} || $::form->get_standard_dbh;
+ my ($sth, $ref, $query);
+
+ $params{months} ||= 6;
+
+ # 1. Auslesen aller erzeugten periodischen Rechnungen im
+ # Betrachtungszeitraum
+ my $q_min_date = $dbh->quote($self->{min_date} . '-01');
+ $query = <<SQL;
+ SELECT pi.config_id, to_char(pi.period_start_date, 'YYYY-MM') AS period_start_date
+ FROM periodic_invoices pi
+ LEFT JOIN periodic_invoices_configs pcfg ON (pi.config_id = pcfg.id)
+ WHERE pcfg.active
+ AND (pi.period_start_date >= to_date($q_min_date, 'YYYY-MM-DD'))
+SQL
+
+ my %periodic_invoices;
+ $sth = prepare_execute_query($::form, $dbh, $query);
+ while ($ref = $sth->fetchrow_hashref) {
+ $periodic_invoices{ $ref->{config_id} } ||= { };
+ $periodic_invoices{ $ref->{config_id} }->{ $ref->{period_start_date} } = 1;
+ }
+ $sth->finish;
+
+ # 2. Auslesen aktiver Wartungsvertragskonfigurationen
+ $query = <<SQL;
+ SELECT (oi.qty * (1 - oi.discount) * oi.sellprice) AS linetotal,
+ bg.description AS buchungsgruppe,
+ CASE WHEN COALESCE(e.name, '') = '' THEN e.login ELSE e.name END AS salesman,
+ pcfg.periodicity, pcfg.id AS config_id,
+ EXTRACT(year FROM pcfg.start_date) AS start_year, EXTRACT(month FROM pcfg.start_date) AS start_month
+ FROM orderitems oi
+ LEFT JOIN oe ON (oi.trans_id = oe.id)
+ LEFT JOIN periodic_invoices_configs pcfg ON (oi.trans_id = pcfg.oe_id)
+ LEFT JOIN parts p ON (oi.parts_id = p.id)
+ LEFT JOIN buchungsgruppen bg ON (p.buchungsgruppen_id = bg.id)
+ LEFT JOIN employee e ON (COALESCE(oe.salesman_id, oe.employee_id) = e.id)
+ WHERE pcfg.active
+SQL
+
+ # 3. Iterieren über Saldierungsintervalle, vormerken
+ my %periodicities = ( 'm' => 1, 'q' => 3, 'y' => 12 );
+ my @scentries;
+ $sth = prepare_execute_query($::form, $dbh, $query);
+ while ($ref = $sth->fetchrow_hashref) {
+ my ($year, $month) = ($ref->{start_year}, $ref->{start_month});
+ my $date;
+
+ while (($date = _the_date($year, $month)) le $self->{max_date}) {
+ if (($date ge $self->{min_date}) && (!$periodic_invoices{ $ref->{config_id} } || !$periodic_invoices{ $ref->{config_id} }->{$date})) {
+ push @scentries, { buchungsgruppe => $ref->{buchungsgruppe},
+ salesman => $ref->{salesman},
+ linetotal => $ref->{linetotal},
+ date => $date,
+ };
+ }
+
+ ($year, $month) = _fix_date($year, $month + ($periodicities{ $ref->{periodicity} } || 1));
+ }
+ }
+ $sth->finish;
+
+ # 4. Auslesen offener Aufträge
+ $query = <<SQL;
+ SELECT (oi.qty * (1 - oi.discount) * oi.sellprice) AS linetotal,
+ bg.description AS buchungsgruppe,
+ CASE WHEN COALESCE(e.name, '') = '' THEN e.login ELSE e.name END AS salesman,
+ oe.ordnumber, EXTRACT(month FROM oe.reqdate) AS month, EXTRACT(year FROM oe.reqdate) AS year
+ FROM orderitems oi
+ LEFT JOIN oe ON (oi.trans_id = oe.id)
+ LEFT JOIN parts p ON (oi.parts_id = p.id)
+ LEFT JOIN buchungsgruppen bg ON (p.buchungsgruppen_id = bg.id)
+ LEFT JOIN employee e ON (COALESCE(oe.salesman_id, oe.employee_id) = e.id)
+ WHERE (oe.customer_id IS NOT NULL)
+ AND NOT COALESCE(oe.quotation, FALSE)
+ AND NOT COALESCE(oe.closed, FALSE)
+ AND (oe.id NOT IN (SELECT oe_id FROM periodic_invoices_configs))
+SQL
+
+ # 5. Initialisierung der Datenstrukturen zum Speichern der
+ # Ergebnisse
+ my @entries = selectall_hashref_query($::form, $dbh, $query);
+ my @salesmen = uniq map { $_->{salesman} } (@entries, @scentries);
+ my @buchungsgruppen = uniq map { $_->{buchungsgruppe} } (@entries, @scentries);
+ my @now = localtime;
+ my @dates = map { $self->_date_for($now[5] + 1900, $now[4] + $_) } (0..$self->{params}->{months} + 1);
+ my %dates_by_ordnumber = map { $_->{ordnumber} => $self->_date_for($_) } @entries;
+ my %salesman_by_ordnumber = map { $_->{ordnumber} => $_->{salesman} } @entries;
+ my %date_sorter = ( old => '0000-00', future => '9999-99' );
+
+ my $projection = { total => { map { $_ => 0 } @dates },
+ order => { map { $_ => 0 } @dates },
+ partial => { map { $_ => 0 } @dates },
+ support => { map { $_ => 0 } @dates },
+ salesman => { map { $_ => { map { $_ => 0 } @dates } } @salesmen },
+ buchungsgruppe => { map { $_ => { map { $_ => 0 } @dates } } @buchungsgruppen },
+ sorted => { month => [ sort { ($date_sorter{$a} || $a) cmp ($date_sorter{$b} || $b) } @dates ],
+ salesman => [ sort { $a cmp $b } @salesmen ],
+ buchungsgruppe => [ sort { $a cmp $b } @buchungsgruppen ],
+ type => [ qw(order partial support) ],
+ },
+ };
+
+ # 6. Aufsummieren der Auftragspositionen
+ foreach $ref (@entries) {
+ my $date = $self->_date_for($ref);
+
+ $projection->{total}->{$date} += $ref->{linetotal};
+ $projection->{order}->{$date} += $ref->{linetotal};
+ $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal};
+ $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal};
+ }
+
+ # 7. Aufsummieren der Wartungsvertragspositionen
+ foreach $ref (@scentries) {
+ my $date = $ref->{date};
+
+ $projection->{total}->{$date} += $ref->{linetotal};
+ $projection->{support}->{$date} += $ref->{linetotal};
+ $projection->{salesman}->{ $ref->{salesman} }->{$date} += $ref->{linetotal};
+ $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} }->{$date} += $ref->{linetotal};
+ }
+
+ if (%dates_by_ordnumber) {
+ # 8. Auslesen von Positionen von Teilrechnungen zu Aufträgen
+ my $ordnumbers = join ', ', map { $dbh->quote($_) } keys %dates_by_ordnumber;
+ $query = <<SQL;
+ SELECT (i.qty * (1 - i.discount) * i.sellprice) AS linetotal,
+ bg.description AS buchungsgruppe,
+ ar.ordnumber
+ FROM invoice i
+ LEFT JOIN ar ON (i.trans_id = ar.id)
+ LEFT JOIN parts p ON (i.parts_id = p.id)
+ LEFT JOIN buchungsgruppen bg ON (p.buchungsgruppen_id = bg.id)
+ WHERE (ar.ordnumber IN ($ordnumbers))
+SQL
+
+ @entries = selectall_hashref_query($::form, $dbh, $query);
+
+ # 9. Abziehen der abgerechneten Positionen
+ foreach $ref (@entries) {
+ my $date = $dates_by_ordnumber{ $ref->{ordnumber} } || die;
+ my $salesman = $salesman_by_ordnumber{ $ref->{ordnumber} } || die;
+ my $buchungsgruppe = $projection->{buchungsgruppe}->{ $ref->{buchungsgruppe} } ? $ref->{buchungsgruppe} : $buchungsgruppen[0];
+
+ $projection->{partial}->{$date} -= $ref->{linetotal};
+ $projection->{total}->{$date} -= $ref->{linetotal};
+ $projection->{salesman}->{$salesman}->{$date} -= $ref->{linetotal};
+ $projection->{buchungsgruppe}->{$buchungsgruppe}->{$date} -= $ref->{linetotal};
+ }
+ }
+
+ return $projection;
+}
+
+# Skaliert '$year' und '$month' so, dass 1 <= Monat <= 12 gilt. Zum
+# Einfachen Addieren gedacht, z.B.
+#
+# my ($new_year, $new_month) = _fix_date($old_year, $old_month + 6);
+
+sub _fix_date {
+ my $year = shift;
+ my $month = shift;
+
+ $year += int(($month - 1) / 12);
+ $month = (($month - 1) % 12 ) + 1;
+
+ ($year, $month);
+}
+
+# Formartiert Jahr & Monat wie benötigt.
+
+sub _the_date {
+ sprintf '%04d-%02d', _fix_date(@_);
+}
+
+# Mappt Datum auf Kategorie. Ist das Datum leer, oder liegt es vor dem
+# Betrachtungszeitraum, so ist die Kategorie 'old'. Liegt das Datum
+# nach dem Betrachtungszeitraum, so ist die Kategorie
+# 'future'. Andernfalls ist sie das formartierte Datum selber.
+
+sub _date_for {
+ my $self = shift;
+ my $ref = ref $_[0] eq 'HASH' ? shift : { year => $_[0], month => $_[1] };
+
+ return 'old' if !$ref->{year} || !$ref->{month};
+
+ my $date = _the_date($ref->{year}, $ref->{month});
+
+ $date lt $self->{min_date} ? 'old'
+ : $date gt $self->{max_date} ? 'future'
+ : $date;
+}
+
+1;
($yy, $mm, $dd) = ($date =~ /(..)(..)(..)/);
}
- $dd *= 1;
- $mm *= 1;
+ $_ ||= 0 for ($dd, $mm, $yy);
+ $_ *= 1 for ($dd, $mm, $yy);
$yy = ($yy < 70) ? $yy + 2000 : $yy;
$yy = ($yy >= 70 && $yy <= 99) ? $yy + 1900 : $yy;
my ($date_str, $time_str) = split m{\s+}, $string, 2;
my ($yy, $mm, $dd) = $self->parse_date(\%params, $date_str);
- my $millisecond = 0;
- my ($hour, $minute, $second) = split m/:/, $time_str;
- ($second, $millisecond) = split quotemeta($num_separator), $second, 2;
+ my ($hour, $minute, $second) = split m/:/, ($time_str || '');
+ $second ||= '0';
+
+ ($second, my $millisecond) = split quotemeta($num_separator), $second, 2;
+ $_ ||= 0 for ($hour, $minute, $millisecond);
+
$millisecond = substr $millisecond, 0, 3;
$millisecond .= '0' x (3 - length $millisecond);
return SL::Auth::evaluate_rights_ary($stack[0]);
}
+sub parse_instance_conf_string {
+ my ($self, $setting) = @_;
+ return $::instance_conf->data->{$setting};
+}
+
sub set_access {
my $self = shift;
my $entry = $self->{$key};
$entry->{GRANTED} = $entry->{ACCESS} ? $self->parse_access_string($key, $entry->{ACCESS}) : 1;
+ $entry->{GRANTED} &&= $self->parse_instance_conf_string($entry->{INSTANCE_CONF}) if $entry->{INSTANCE_CONF};
$entry->{IS_MENU} = $entry->{submenu} || ($key !~ m/--/);
$entry->{NUM_VISIBLE_CHILDREN} = 0;
our @EXPORT = qw(save_form restore_form compare_numbers cross);
our @EXPORT_OK = qw(ary_union ary_intersect ary_diff listify ary_to_hash uri_encode uri_decode);
+use Encode ();
use List::MoreUtils qw(zip);
use YAML;
qq| ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id, | .
qq| tz.description AS taxzone | .
$periodic_invoices_columns .
+ qq| , o.order_probability, o.expected_billing_date, (o.netamount * o.order_probability / 100) AS expected_netamount | .
qq|FROM oe o | .
qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
qq|LEFT JOIN employee e ON (o.employee_id = e.id) | .
$query .= qq| AND ${not} COALESCE(pcfg.active, 'f')|;
}
+ if ($form->{reqdate_unset_or_old}) {
+ $query .= qq| AND ((o.reqdate IS NULL) OR (o.reqdate < date_trunc('month', current_date)))|;
+ }
+
+ if (($form->{order_probability_value} || '') ne '') {
+ my $op = $form->{order_probability_value} eq 'le' ? '<=' : '>=';
+ $query .= qq| AND (o.order_probability ${op} ?)|;
+ push @values, $form->{order_probability_value};
+ }
+
+ if ($form->{expected_billing_date_from}) {
+ $query .= qq| AND (o.expected_billing_date >= ?)|;
+ push @values, conv_date($form->{expected_billing_date_from});
+ }
+
+ if ($form->{expected_billing_date_to}) {
+ $query .= qq| AND (o.expected_billing_date <= ?)|;
+ push @values, conv_date($form->{expected_billing_date_to});
+ }
+
my $sortdir = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name")));
my %allowed_sort_columns = (
delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
+ , order_probability = ?, expected_billing_date = ?
WHERE id = ?|;
@values = ($form->{ordnumber} || '', $form->{quonumber},
conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
$form->{transaction_description},
$form->{marge_total} * 1, $form->{marge_percent} * 1,
+ $form->{order_probability} * 1, conv_date($form->{expected_billing_date}),
conv_i($form->{id}));
do_query($form, $dbh, $query, @values);
$form->{ordtotal} = $amount;
- # add shipto
$form->{name} = $form->{ $form->{vc} };
$form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//;
+ # add shipto
if (!$form->{shipto_id}) {
$form->add_shipto($dbh, $form->{id}, "OE");
}
d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id
+ , o.order_probability, o.expected_billing_date
FROM oe o
JOIN ${vc} cv ON (o.${vc}_id = cv.id)
LEFT JOIN employee e ON (o.employee_id = e.id)
$form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
$form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
+ $::form->{order} = SL::DB::Manager::Order->find_by(id => $::form->{id});
+
$main::lxdebug->leave_sub();
}
my @headingaccounts = ();
my $dpt_where;
my $dpt_where_without_arapgl;
+ my ($customer_where, $customer_join, $customer_no_union);
my $project;
my $where = "1 = 1";
(SELECT department_id FROM gl WHERE gl.id=ac.trans_id),
(SELECT department_id FROM ap WHERE ap.id=ac.trans_id)) = | . conv_i($department_id);
}
+ if ($form->{customer_id}) {
+ $customer_join = qq| JOIN ar a ON (ac.trans_id = a.id) |;
+ $customer_where = qq| AND (a.customer_id = | . conv_i($form->{customer_id}, 'NULL') . qq|) |;
+ $customer_no_union = qq| AND 1=0 |;
+ }
# project_id only applies to getting transactions
# it has nothing to do with a trial balance
my $min_max = $prefix eq 'from' ? 'min' : 'max';
$query = qq|SELECT ${min_max}(transdate)
FROM acc_trans ac
+ $customer_join
WHERE (1 = 1)
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project|;
($form->{"${prefix}date"}) = selectfirst_array_query($form, $dbh, $query);
}
qq|SELECT c.accno, c.category, SUM(ac.amount) AS amount, c.description
FROM acc_trans ac
LEFT JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE ((select date_trunc('year', ac.transdate::date)) = (select date_trunc('year', ?::date))) AND ac.ob_transaction
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
GROUP BY c.accno, c.category, c.description |;
SELECT c.accno, c.description, c.category, SUM(ac.amount) AS amount
FROM acc_trans ac
JOIN chart c ON (c.id = ac.chart_id)
+ $customer_join
WHERE $where
$dpt_where_without_arapgl
$project
JOIN chart c ON (p.income_accno_id = c.id)
WHERE $invwhere
$dpt_where
+ $customer_where
$project
GROUP BY c.accno, c.description, c.category
JOIN chart c ON (p.expense_accno_id = c.id)
WHERE $invwhere
$dpt_where
+ $customer_no_union
$project
GROUP BY c.accno, c.description, c.category
|;
(SELECT SUM(ac.amount) * -1
FROM acc_trans ac
JOIN chart c ON (c.id = ac.chart_id)
+ $customer_join
WHERE $where
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND (ac.amount < 0)
AND (c.accno = ?)) AS debit,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (c.id = ac.chart_id)
+ $customer_join
WHERE $where
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND ac.amount > 0
AND c.accno = ?) AS credit,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $saldowhere
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND c.accno = ? AND (NOT ac.ob_transaction OR ac.ob_transaction IS NULL)) AS saldo,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $sumwhere
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
- AND amount > 0
+ AND ac.amount > 0
AND c.accno = ?) AS sum_credit,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $sumwhere
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
- AND amount < 0
+ AND ac.amount < 0
AND c.accno = ?) AS sum_debit,
(SELECT max(ac.transdate) FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $where
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND c.accno = ?) AS last_transaction
JOIN chart c ON (p.expense_accno_id = c.id)
WHERE $invwhere
$dpt_where
+ $customer_no_union
$project
AND c.accno = ?) AS debit,
JOIN chart c ON (p.income_accno_id = c.id)
WHERE $invwhere
$dpt_where
+ $customer_where
$project
AND c.accno = ?) AS credit,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $saldowhere
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND c.accno = ? AND (NOT ac.ob_transaction OR ac.ob_transaction IS NULL)) AS saldo,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $sumwhere
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
- AND amount > 0
+ AND ac.amount > 0
AND c.accno = ?) AS sum_credit,
(SELECT SUM(ac.amount)
FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $sumwhere
+ $dpt_where
$dpt_where_without_arapgl
+ $customer_where
$project
- AND amount < 0
+ AND ac.amount < 0
AND c.accno = ?) AS sum_debit,
(SELECT max(ac.transdate) FROM acc_trans ac
JOIN chart c ON (ac.chart_id = c.id)
+ $customer_join
WHERE $where
$dpt_where_without_arapgl
+ $dpt_where
+ $customer_where
$project
AND c.accno = ?) AS last_transaction
|;
$row->{$column}->{align} = $self->{columns}->{$column}->{align} unless (defined $row->{$column}->{align});
}
- foreach my $field (qw(data link)) {
+ foreach my $field (qw(data link link_class)) {
map { $row->{$_}->{$field} = [ $row->{$_}->{$field} ] if (ref $row->{$_}->{$field} ne 'ARRAY') } keys %{ $row };
}
}
push @{ $col->{CELL_ROWS} }, {
'data' => '' . $self->html_format($col->{data}->[$i]),
'link' => $col->{link}->[$i],
+ link_class => $col->{link_class}->[$i],
};
}
return 'html';
}
+sub cache {
+ my ($self, $topic, $default) = @_;
+
+ $topic = '::' . (caller(0))[0] . "::$topic" unless $topic =~ m{^::};
+
+ $self->{_cache} //= {};
+ $self->{_cache}->{$topic} //= ($default // {});
+
+ return $self->{_cache}->{$topic};
+}
+
sub _store_value {
my ($target, $key, $value) = @_;
my @tokens = split /((?:\[\+?\])?(?:\.)|(?:\[\+?\]))/, $key;
For more information about layouts, see L<SL::Layout::Dispatcher>.
+=item C<cache $topic[, $default ]>
+
+Caches an item for the duration of the request. C<$topic> must be an
+index name referring to the thing to cache. It is used for retrieving
+it later on. If C<$topic> doesn't start with C<::> then the caller's
+package name is prepended to the topic. For example, if the a from
+package C<SL::StuffedStuff> calls with topic = C<get_stuff> then the
+actual key will be C<::SL::StuffedStuff::get_stuff>.
+
+If no item exists in the cache for C<$topic> then it is created and
+its initial value is set to C<$default>. If C<$default> is not given
+(undefined) then a new, empty hash reference is created.
+
+Returns the cached item.
+
=back
=head1 SPECIAL FUNCTIONS
$form->{"Z45"} = $form->{"Z43"};
- $form->{"Z53"} = $form->{"Z45"} + $form->{"53"} + $form->{"74"}
+ $form->{"Z53"} = $form->{"Z45"} + $form->{"47"} + $form->{"53"} + $form->{"74"}
+ $form->{"85"} + $form->{"65"};
$form->{"Z62"} = $form->{"Z43"} - $form->{"66"} - $form->{"61"}
my $arwhere = "";
my $item;
- my $gltaxkey_where = "((tk.pos_ustva>=59 AND tk.pos_ustva<=66) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))";
+ my $gltaxkey_where = "((tk.pos_ustva = 46) OR (tk.pos_ustva>=59 AND tk.pos_ustva<=66) or (tk.pos_ustva>=89 AND tk.pos_ustva<=93))";
if ($fromdate) {
if ($form->{method} eq 'cash') {
&dbconnect_vars($form, $db);
+ # Flush potentially held database locks.
+ $form->get_standard_dbh->commit;
+
my $dbh = SL::DBConnect->connect($form->{dbconnect}, $form->{dbuser}, $form->{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
$dbh->do($form->{dboptions}) if ($form->{dboptions});
use POSIX qw(strftime);
use SL::CT;
+use SL::CTI;
use SL::CVar;
use SL::Request qw(flatten);
use SL::DB::Business;
}
my @columns = (
- 'id', 'name', "$form->{db}number", 'contact', 'phone', 'discount',
- 'fax', 'email', 'taxnumber', 'street', 'zipcode' , 'city',
- 'business', 'invnumber', 'ordnumber', 'quonumber', 'salesman', 'country'
+ 'id', 'name', "$form->{db}number", 'contact', 'phone', 'discount',
+ 'fax', 'email', 'taxnumber', 'street', 'zipcode' , 'city',
+ 'business', 'payment', 'invnumber', 'ordnumber', 'quonumber', 'salesman',
+ 'country'
);
my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
'country' => { 'text' => $locale->text('Country'), },
'salesman' => { 'text' => $locale->text('Salesman'), },
'discount' => { 'text' => $locale->text('Discount'), },
+ 'payment' => { 'text' => $locale->text('Payment Terms'), },
%column_defs_cvars,
);
my $column = $ref->{formtype} eq 'invoice' ? 'invnumber' : $ref->{formtype} eq 'order' ? 'ordnumber' : 'quonumber';
$row->{$column}->{data} = $ref->{$column};
+ if (my $number = SL::CTI->sanitize_number(number => $ref->{phone})) {
+ $row->{phone}->{link} = SL::CTI->call_link(number => $number);
+ $row->{phone}->{link_class} = 'cti_call_action';
+ }
+
$report->add_data($row);
}
$row->{$_}->{link} = 'mailto:' . E($ref->{$_}) if $ref->{$_};
}
+ for (qw(cp_phone1 cp_phone2 cp_mobile1)) {
+ next unless my $number = SL::CTI->sanitize_number(number => $ref->{$_});
+
+ $row->{$_}->{link} = SL::CTI->call_link(number => $number);
+ $row->{$_}->{link_class} = 'cti_call_action';
+ }
+
$report->add_data($row);
}
# Delivery orders
#======================================================================
+use List::MoreUtils qw(uniq);
use List::Util qw(max sum);
use POSIX qw(strftime);
use YAML;
check_do_access();
+ if (($::form->{type} =~ /purchase/) && !$::instance_conf->get_allow_new_purchase_invoice) {
+ $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
+ }
+
my $form = $main::form;
set_headings("add");
'ids' => $form->{id});
$form->backup_vars(qw(payment_id language_id taxzone_id salesman_id taxincluded cp_id intnotes delivery_term_id currency));
- $form->{shipto} = 1 if $form->{id} || $form->{convert_from_oe_ids};
# get customer / vendor
if ($form->{vc} eq 'vendor') {
$form->{employee_id} = $form->{old_employee_id} if $form->{old_employee_id};
$form->{salesman_id} = $form->{old_salesman_id} if $form->{old_salesman_id};
- my @old_project_ids = ($form->{"globalproject_id"});
- map({ push(@old_project_ids, $form->{"project_id_$_"})
- if ($form->{"project_id_$_"}); } (1..$form->{"rowcount"}));
-
my $vc = $form->{vc} eq "customer" ? "customers" : "vendors";
- $form->get_lists("projects" => {
- "key" => "ALL_PROJECTS",
- "all" => 0,
- "old_id" => \@old_project_ids
- },
- $vc => "ALL_VC",
+ $form->get_lists($vc => "ALL_VC",
"price_factors" => "ALL_PRICE_FACTORS",
"departments" => "ALL_DEPARTMENTS",
"business_types" => "ALL_BUSINESS_TYPES",
);
+ # Projects
+ my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
+ my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : ();
+ my @customer_cond;
+ if (($vc eq 'customers') && $::instance_conf->get_customer_projects_only_in_sales) {
+ @customer_cond = (
+ or => [
+ customer_id => $::form->{customer_id},
+ billable_customer_id => $::form->{customer_id},
+ ]);
+ }
+ my @conditions = (
+ or => [
+ and => [ active => 1, @customer_cond ],
+ @old_ids_cond,
+ ]);
+
+ $::form->{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions);
$::form->{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id}, deleted => 0 ] ]);
$::form->{ALL_SALESMEN} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{salesman_id}, deleted => 0 ] ]);
$::form->{ALL_SHIPTO} = SL::DB::Manager::Shipto->get_all_sorted(query => [
require "bin/mozilla/$form->{script}";
my $currency = $form->{currency};
- $form->{shipto} = 1 if $form->{convert_from_do_ids};
invoice_links();
if ($form->{ordnumber}) {
use SL::IC;
use SL::IO;
+use SL::DB::Customer;
use SL::DB::Default;
use SL::DB::Language;
use SL::DB::Printer;
+use SL::DB::Vendor;
use SL::Helper::CreatePDF;
use SL::Helper::Flash;
}
$form->{script} = 'oe.pl';
- $form->{shipto} = 1;
-
$form->{rowcount}--;
$form->{cp_id} *= 1;
$form->{script} = 'oe.pl';
- $form->{shipto} = 1;
-
$form->{rowcount}--;
require "bin/mozilla/$form->{script}";
$form->error($::locale->text('No print templates have been created for this client yet. Please do so in the client configuration.')) if !$defaults->templates;
$form->{templates} = $defaults->templates;
- my ($old_form) = @_;
+ my ($old_form, %params) = @_;
my $inv = "inv";
my $due = "due";
$form->get_shipto(\%myconfig);
}
- my @a = qw(name department_1 department_2 street zipcode city country contact phone fax email);
-
- my $shipto = 1;
-
- # if there is no shipto fill it in from billto
- foreach my $item (@a) {
- if ($form->{"shipto$item"}) {
- $shipto = 0;
- last;
- }
- }
-
- if ($shipto) {
- if ( $form->{formname} eq 'purchase_order'
- || $form->{formname} eq 'request_quotation') {
- $form->{shiptoname} = $defaults->company;
- $form->{shiptostreet} = $defaults->address;
- } else {
- map { $form->{"shipto$_"} = $form->{$_} } @a;
- }
- }
-
$form->{notes} =~ s/^\s+//g;
delete $form->{printer_command};
($form->{media} eq 'printer')
? $locale->text('sent to printer')
: $locale->text('emailed to') . " $form->{email}";
- $form->redirect(qq|$form->{label} $form->{"${inv}number"} $msg|);
+
+ if (!$params{no_redirect}) {
+ $form->redirect(qq|$form->{label} $form->{"${inv}number"} $msg|);
+ }
}
if ($form->{printing}) {
call_sub($display_form);
$::form->{title} = $::locale->text('Ship to');
$::form->header;
+ my $vc_obj = ($::form->{vc} eq 'customer' ? "SL::DB::Customer" : "SL::DB::Vendor")->new(id => $::form->{$::form->{vc} . "_id"})->load;
+
print $::form->parse_html_template('io/ship_to', { previousform => $previous_form,
nextsub => $::form->{display_form} || 'display_form',
+ vc_obj => $vc_obj,
});
$main::lxdebug->leave_sub();
$main::auth->assert('vendor_invoice_edit');
+ if (!$::instance_conf->get_allow_new_purchase_invoice) {
+ $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
+ }
+
return $main::lxdebug->leave_sub() if (load_draft_maybe());
$form->{title} = $locale->text('Record Vendor Invoice');
use SL::OE;
use Data::Dumper;
use DateTime;
+use List::MoreUtils qw(uniq);
use List::Util qw(max sum);
use SL::DB::Default;
taxincluded currency cp_id intnotes id shipto_id
delivery_term_id));
- $form->{shipto} = 1 if $editing || $form->{convert_from_oe_ids} || $form->{convert_from_do_ids};
IS->get_customer(\%myconfig, \%$form);
#quote all_customer Bug 133
$form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
- my @old_project_ids = ($form->{"globalproject_id"});
- map { push @old_project_ids, $form->{"project_id_$_"} if $form->{"project_id_$_"}; } 1..$form->{"rowcount"};
-
- $form->get_lists("projects" => { "key" => "ALL_PROJECTS",
- "all" => 0,
- "old_id" => \@old_project_ids },
- "taxzones" => "ALL_TAXZONES",
+ $form->get_lists("taxzones" => "ALL_TAXZONES",
"currencies" => "ALL_CURRENCIES",
"customers" => "ALL_CUSTOMERS",
"departments" => "all_departments",
"price_factors" => "ALL_PRICE_FACTORS");
+ # Projects
+ my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
+ my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : ();
+ my @customer_cond;
+ if ($::instance_conf->get_customer_projects_only_in_sales) {
+ @customer_cond = (
+ or => [
+ customer_id => $::form->{customer_id},
+ billable_customer_id => $::form->{customer_id},
+ ]);
+ }
+ my @conditions = (
+ or => [
+ and => [ active => 1, @customer_cond ],
+ @old_ids_cond,
+ ]);
+
+ $TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions);
$TMPL_VAR{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id}, deleted => 0 ] ]);
$TMPL_VAR{ALL_SALESMEN} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{salesman_id}, deleted => 0 ] ]);
$TMPL_VAR{ALL_SHIPTO} = SL::DB::Manager::Shipto->get_all_sorted(query => [
$form->{id} = '';
$form->{rowcount}--;
- $form->{shipto} = 1;
$form->{title} = $locale->text('Add Credit Note');
use SL::MoreCommon qw(ary_diff);
use SL::PE;
use SL::ReportGenerator;
-use List::MoreUtils qw(any none);
+use List::MoreUtils qw(uniq any none);
use List::Util qw(min max reduce sum);
use Data::Dumper;
$main::auth->assert($right);
}
+sub check_oe_conversion_to_sales_invoice_allowed {
+ return 1 if $::form->{type} !~ m/^sales/;
+ return 1 if ($::form->{type} =~ m/quotation/) && $::instance_conf->get_allow_sales_invoice_from_sales_quotation;
+ return 1 if ($::form->{type} =~ m/order/) && $::instance_conf->get_allow_sales_invoice_from_sales_order;
+
+ $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
+
+ return 0;
+}
+
sub set_headings {
$main::lxdebug->enter_sub();
$form->{"$form->{vc}_id"} ||= $form->{"all_$form->{vc}"}->[0]->{id} if $form->{"all_$form->{vc}"};
$form->backup_vars(qw(payment_id language_id taxzone_id salesman_id taxincluded cp_id intnotes shipto_id delivery_term_id currency));
- $form->{shipto} = 1 if $form->{id} || $form->{convert_from_oe_ids};
# get customer / vendor
IR->get_vendor(\%myconfig, \%$form) if $form->{type} =~ /(purchase_order|request_quotation)/;
$form->{"closed"} ? "checked" : "", $locale->text('Closed') if $form->{id};
$TMPL_VAR{openclosed} = sprintf qq|<tr><td colspan=%d align=center>%s</td></tr>\n|, 2 * scalar @tmp, join "\n", @tmp if @tmp;
- # project ids
- my @old_project_ids = ($form->{"globalproject_id"}, grep { $_ } map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
-
my $vc = $form->{vc} eq "customer" ? "customers" : "vendors";
- $form->get_lists("projects" => { "key" => "ALL_PROJECTS",
- "all" => 0,
- "old_id" => \@old_project_ids },
- "taxzones" => "ALL_TAXZONES",
+
+ # project ids
+ $form->get_lists("taxzones" => "ALL_TAXZONES",
"payments" => "ALL_PAYMENTS",
"currencies" => "ALL_CURRENCIES",
"departments" => "ALL_DEPARTMENTS",
limit => $myconfig{vclimit} + 1 },
"price_factors" => "ALL_PRICE_FACTORS");
+ # Projects
+ my @old_project_ids = uniq grep { $_ } map { $_ * 1 } ($form->{"globalproject_id"}, map { $form->{"project_id_$_"} } 1..$form->{"rowcount"});
+ my @old_ids_cond = @old_project_ids ? (id => \@old_project_ids) : ();
+ my @customer_cond;
+ if (($vc eq 'customers') && $::instance_conf->get_customer_projects_only_in_sales) {
+ @customer_cond = (
+ or => [
+ customer_id => $::form->{customer_id},
+ billable_customer_id => $::form->{customer_id},
+ ]);
+ }
+ my @conditions = (
+ or => [
+ and => [ active => 1, @customer_cond ],
+ @old_ids_cond,
+ ]);
+
+ $TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all(query => \@conditions);
+
# label subs
my $employee_list_query_gen = sub { $::form->{$_[0]} ? [ or => [ id => $::form->{$_[0]}, deleted => 0 ] ] : [ deleted => 0 ] };
$TMPL_VAR{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => $employee_list_query_gen->('employee_id'));
is_pur_ord => scalar ($form->{type} =~ /purchase_order$/),
);
+ $TMPL_VAR{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+
print $form->parse_html_template("oe/form_header", { %TMPL_VAR });
$main::lxdebug->leave_sub();
# constants and subs for template
$form->{vc_keys} = sub { "$_[0]->{name}--$_[0]->{id}" };
+ $form->{ORDER_PROBABILITIES} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
+
$form->header();
print $form->parse_html_template('oe/search', {
"vcnumber", "ustid",
"country", "shippingpoint",
"taxzone",
+ "order_probability", "expected_billing_date", "expected_netamount",
);
# only show checkboxes if gotten here via sales_order form.
$form->{l_delivered} = "Y" if ($form->{delivered} && $form->{notdelivered});
$form->{l_periodic_invoices} = "Y" if ($form->{periodic_invoices_active} && $form->{periodic_invoices_inactive});
+ map { $form->{"l_${_}"} = 'Y' } qw(order_probability expected_billing_date expected_netamount) if $form->{l_order_probability_expected_billing_date};
+
my $attachment_basename;
if ($form->{vc} eq 'vendor') {
if ($form->{type} eq 'purchase_order') {
push @hidden_variables, "l_subtotal", $form->{vc}, qw(l_closed l_notdelivered open closed delivered notdelivered ordnumber quonumber cusordnumber
transaction_description transdatefrom transdateto type vc employee_id salesman_id
reqdatefrom reqdateto projectnumber project_id periodic_invoices_active periodic_invoices_inactive
- business_id shippingpoint taxzone_id);
+ business_id shippingpoint taxzone_id reqdate_unset_or_old
+ order_probability_op order_probability_value expected_billing_date_from expected_billing_date_to);
my @keys_for_url = grep { $form->{$_} } @hidden_variables;
push @keys_for_url, 'taxzone_id' if $form->{taxzone_id} ne ''; # taxzone_id could be 0
'periodic_invoices' => { 'text' => $locale->text('Per. Inv.'), },
'shippingpoint' => { 'text' => $locale->text('Shipping Point'), },
'taxzone' => { 'text' => $locale->text('Steuersatz'), },
+ 'order_probability' => { 'text' => $locale->text('Order probability'), },
+ 'expected_billing_date' => { 'text' => $locale->text('Exp. bill. date'), },
+ 'expected_netamount' => { 'text' => $locale->text('Exp. netamount'), },
);
foreach my $name (qw(id transdate reqdate quonumber ordnumber cusordnumber name employee salesman shipvia transaction_description shippingpoint taxzone)) {
$column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
}
- my %column_alignment = map { $_ => 'right' } qw(netamount tax amount curr remaining_amount remaining_netamount);
+ my %column_alignment = map { $_ => 'right' } qw(netamount tax amount curr remaining_amount remaining_netamount order_probability expected_billing_date expected_netamount);
$form->{"l_type"} = "Y";
map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
push @options, $locale->text('Delivery Order created') if $form->{delivered};
push @options, $locale->text('Not delivered') if $form->{notdelivered};
push @options, $locale->text('Periodic invoices active') if $form->{periodic_invoices_active};
+ push @options, $locale->text('Reqdate not set or before current month') if $form->{reqdate_unset_or_old};
if ($form->{business_id}) {
my $vc_type_label = $form->{vc} eq 'customer' ? $locale->text('Customer type') : $locale->text('Vendor type');
push @options, $locale->text('Steuersatz') . " : " . SL::DB::TaxZone->new(id => $form->{taxzone_id})->load->description;
}
+ if (($form->{order_probability_value} || '') ne '') {
+ push @options, $::locale->text('Order probability') . ' ' . ($form->{order_probability_op} eq 'le' ? '<=' : '>=') . ' ' . $form->{order_probability_value} . '%';
+ }
+
+ if ($form->{expected_billing_date_from} or $form->{expected_billing_date_to}) {
+ push @options, $locale->text('Expected billing date');
+ push @options, $locale->text('From') . " " . $locale->date(\%myconfig, $form->{expected_billing_date_from}, 1) if $form->{expected_billing_date_from};
+ push @options, $locale->text('Bis') . " " . $locale->date(\%myconfig, $form->{expected_billing_date_to}, 1) if $form->{expected_billing_date_to};
+ }
+
$report->set_options('top_info_text' => join("\n", @options),
'raw_top_info_text' => $form->parse_html_template('oe/orders_top'),
'raw_bottom_info_text' => $form->parse_html_template('oe/orders_bottom', { 'SHOW_CONTINUE_BUTTON' => $allow_multiple_orders }),
my $callback = $form->escape($href);
my @subtotal_columns = qw(netamount amount marge_total marge_percent remaining_amount remaining_netamount);
+ push @subtotal_columns, 'expected_netamount' if $form->{l_order_probability_expected_billing_date};
my %totals = map { $_ => 0 } @subtotal_columns;
my %subtotals = map { $_ => 0 } @subtotal_columns;
$subtotals{marge_percent} = $subtotals{netamount} ? ($subtotals{marge_total} * 100 / $subtotals{netamount}) : 0;
$totals{marge_percent} = $totals{netamount} ? ($totals{marge_total} * 100 / $totals{netamount} ) : 0;
- map { $oe->{$_} = $form->format_amount(\%myconfig, $oe->{$_}, 2) } qw(netamount tax amount marge_total marge_percent remaining_amount remaining_netamount);
+ map { $oe->{$_} = $form->format_amount(\%myconfig, $oe->{$_}, 2) } qw(netamount tax amount marge_total marge_percent remaining_amount remaining_netamount expected_netamount);
+
+ $oe->{order_probability} = ($oe->{order_probability} || 0) . '%';
my $row = { };
my $locale = $main::locale;
check_oe_access();
+ check_oe_conversion_to_sales_invoice_allowed();
$main::auth->assert($form->{type} eq 'purchase_order' || $form->{type} eq 'request_quotation' ? 'vendor_invoice_edit' : 'invoice_edit');
$form->{old_salesman_id} = $form->{salesman_id};
$form->{convert_from_oe_ids} = $form->{id};
$form->{transdate} = $form->{invdate} = $form->current_date(\%myconfig);
$form->{duedate} = $form->current_date(\%myconfig, $form->{invdate}, $form->{terms} * 1);
- $form->{shipto} = 1;
$form->{defaultcurrency} = $form->get_default_currency(\%myconfig);
delete @{$form}{qw(id closed)};
$form->{direct_delivery_checked} = 1;
delete @{$form}{grep /^shipto/, keys %{ $form }};
map { s/^CFDD_//; $form->{$_} = $form->{"CFDD_${_}"} } grep /^CFDD_/, keys %{ $form };
- $form->{shipto} = 1;
$form->{CFDD_shipto} = 1;
purchase_order();
$main::lxdebug->leave_sub();
if ($form->{type} eq "purchase_order") {
delete($form->{ordnumber});
+ $form->{"lastcost_$_"} = $form->{"sellprice_$_"} for (1..$form->{rowcount});
}
$form->{cp_id} *= 1;
use SL::DB::Default;
use SL::DB::Project;
+use SL::DB::Customer;
use SL::PE;
use SL::RP;
use SL::Iconv;
);
$::form->{title} = $title{$::form->{report}};
+ $::request->{layout}->add_javascripts('autocomplete_customer.js');
# get departments
$::form->all_departments(\%::myconfig);
$form->{company} = $locale->text('Company') . " " . $defaults->company;
push (@options, $form->{company});
+ if ($::form->{customer_id}) {
+ my $customer = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id});
+ push @options, $::locale->text('Customer') . ' ' . $customer->displayable_name;
+ }
+
$form->{template_to} = $locale->date(\%myconfig, $form->{todate}, 0);
# If set to 1 each exception will include a full stack backtrace.
backtrace_on_die = 0
+
+[cti]
+# If you want phone numbers to be clickable then this must be set to a
+# command that does the actually dialing. Within this command three
+# variables are replaced before it is executed:
+#
+# 1. <%phone_extension%> and <%phone_password%> are taken from the user
+# configuration (changeable in the admin interface).
+# 2. <%number%> is the number to dial. It has already been sanitized
+# and formatted correctly regarding e.g. the international dialing
+# prefix.
+#
+# The following is an example that works with the OpenUC telephony
+# server:
+# dial_command = curl --insecure -X PUT https://<%phone_extension%>:<%phone_password%>@IP.AD.DR.ESS:8443/sipxconfig/rest/my/call/<%number%>
+dial_command =
+# If you need to dial something before the actual number then set
+# external_prefix to it.
+external_prefix = 0
+# The prefix for international calls (numbers starting with +).
+international_dialing_prefix = 00
span.toggle_selected {
font-weight: bold;
}
+
+/* CTI */
+a.cti_call_action {
+ display: inline-block;
+ padding-left: 18px;
+ height: 16px;
+ position: relative;
+ top: 2px;
+ vertical-align: center;
+ background-image: url(../../image/icons/16x16/phone.png);
+ background-repeat: no-repeat;
+}
span.toggle_selected {
font-weight: bold;
}
+
+/* CTI */
+a.cti_call_action {
+ display: inline-block;
+ padding-left: 18px;
+ height: 16px;
+ position: relative;
+ top: 2px;
+ vertical-align: center;
+ background-image: url(../../image/icons/16x16/phone.png);
+ background-repeat: no-repeat;
+}
--- /dev/null
+phone.png
\ No newline at end of file
last_dummy = $dummy.val();
$real.trigger('change');
- if (o.fat_set_item) {
+ if (o.fat_set_item && item.id) {
$.ajax({
url: 'controller.pl?action=Part/show.json',
data: { id: item.id },
// alert(url);
window.open(url, "_new_generic", parm);
}
-
-function warn_save_active_periodic_invoice() {
- return confirm(kivi.t8('This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?'));
-}
'#country'
];
- this.MapWidget = function(prefix)
+ this.MapWidget = function(prefix, source_address)
{
var $mapSearchElements = [];
var $widgetWrapper;
searchString += stmt;
}
- var url = 'https://maps.google.com/maps?q='+ encodeURIComponent(searchString);
+ source_address = source_address || '';
+ var query = source_address != '' ? 'saddr=' + encodeURIComponent(source_address) + '&daddr=' : 'q=';
+ var url = 'https://maps.google.com/maps?' + query + encodeURIComponent(searchString);
window.open(url, '_blank');
window.focus();
var url = "common.pl?INPUT_ENCODING=UTF-8&action=show_history&longdescription=&input_name="+ encodeURIComponent(id);
window.open(url, "_new_generic", parm);
};
+
+ this.update_dial_action = function($input) {
+ var $action = $('#' + $input.prop('id') + '-dial-action');
+
+ if (!$action)
+ return true;
+
+ var number = $input.val().replace(/\s+/g, '');
+ if (number == '')
+ $action.hide();
+ else
+ $action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show();
+
+ return true;
+ };
+
+ this.init_dial_action = function(input) {
+ if ($('#_cti_enabled').val() != 1)
+ return false;
+
+ var $input = $(input);
+ var action_id = $input.prop('id') + '-dial-action';
+
+ if (!$('#' + action_id).size()) {
+ var $action = $('<a href="" id="' + action_id + '" class="cti_call_action" target="_blank" tabindex="-1"></a>');
+ $input.wrap('<span nobr></span>').after($action);
+
+ $input.change(function() { kivi.CustomerVendor.update_dial_action($input); });
+ }
+
+ kivi.CustomerVendor.update_dial_action($input);
+
+ return true;
+ };
});
+
+function local_reinit_widgets() {
+ $('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) {
+ kivi.CustomerVendor.init_dial_action($(elt));
+ });
+}
$element.val($edit.val());
$('#edit_longdescription_dialog').dialog('close');
};
+
+ this.delivery_order_check_transfer_qty = function() {
+ var all_match = true;
+ var rowcount = $('input[name=rowcount]').val();
+ for (var i = 1; i < rowcount; i++)
+ if ($('#stock_in_out_qty_matches_' + i).val() != 1)
+ all_match = false;
+
+ if (all_match)
+ return true;
+
+ return confirm(kivi.t8('There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?'));
+ };
+
+ this.oe_warn_save_active_periodic_invoice = function() {
+ return confirm(kivi.t8('This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?'));
+ };
+
+ this.check_transaction_description = function() {
+ if ($('#transaction_description').val() != '')
+ return true;
+
+ alert(kivi.t8('A transaction description is required.'));
+ return false;
+ };
+
+ this.on_submit_checks = function() {
+ var $button = $(this);
+ if (($button.data('check-transfer-qty') == 1) && !kivi.SalesPurchase.delivery_order_check_transfer_qty())
+ return false;
+
+ if (($button.data('warn-save-active-periodic-invoice') == 1) && !kivi.SalesPurchase.oe_warn_save_active_periodic_invoice())
+ return false;
+
+ if (($button.data('require-transaction-description') == 1) && !kivi.SalesPurchase.check_transaction_description())
+ return false;
+
+ return true;
+ };
+
+ this.init_on_submit_checks = function() {
+ $('input[type=submit]').click(kivi.SalesPurchase.on_submit_checks);
+ };
});
namespace("kivi").setupLocale({
+"A transaction description is required.":"Die Vorgangsbezeichnung muss eingegeben werden.",
"Add function block":"Funktionsblock hinzufügen",
"Add linked record":"Verknüpften Beleg hinzufügen",
"Add picture":"Bild hinzufügen",
"The name is missing.":"Der Name fehlt.",
"The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.",
"The option field is empty.":"Das Optionsfeld ist leer.",
+"The recipient, subject or body is missing.":"Der Empfäger, der Betreff oder der Text ist leer.",
"The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?",
+"There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?":"Einige der Lagerbewegungen sind nicht vollständig und Lagerbewegungen können nachträglich nicht mehr verändert werden. Wollen Sie wirklich fortfahren?",
"There is one or more sections for which no part has been assigned yet; therefore creating the new record is not possible yet.":"Es gibt einen oder mehrere Abschnitte ohne Artikelzuweisung; daher kann der neue Beleg noch nicht erstellt werden.",
"This sales order has an active configuration for periodic invoices. If you save then all subsequently created invoices will contain those changes as well, but not those that have already been created. Do you want to continue?":"Dieser Auftrag besitzt eine aktive Konfiguration für wiederkehrende Rechnungen. Wenn Sie jetzt speichern, so werden alle zukünftig hieraus erzeugten Rechnungen die Änderungen enthalten, nicht aber die bereits erzeugten Rechnungen. Wollen Sie speichern?",
"Time/cost estimate actions":"Aktionen für Kosten-/Zeitabschätzung",
'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => 'Die Bedienung von kivitendo wurde an vielen Stellen mit Javascript verbessert. Obwohl es derzeit möglich ist, jeden Aspekt von kivitendo auch ohne Javascript zu benutzen, empfehlen wir es. In einer zukünftigen Version wird Javascript eventuell notwendig sein um weitergehende Features zu benutzen.',
'A lower-case character is required.' => 'Ein Kleinbuchstabe ist vorgeschrieben.',
'A special character is required (valid characters: #1).' => 'Ein Sonderzeichen ist vorgeschrieben (gültige Zeichen: #1).',
+ 'A transaction description is required.' => 'Die Vorgangsbezeichnung muss eingegeben werden.',
'A unit with this name does already exist.' => 'Eine Einheit mit diesem Namen existiert bereits.',
'A valid taxkey is missing!' => 'Einen gültiger Steuerschlüssel fehlt!',
'A variable marked as \'editable\' can be changed in each quotation, order, invoice etc.' => 'Eine als \'editierbar\' markierte Variable kann in jedem Angebot, Auftrag, jeder Rechnung etc für jede Position geändert werden.',
'All units have either no or exactly one base unit of which they are multiples.' => 'Einheiten haben entweder keine oder genau eine Basiseinheit, von der sie ein Vielfaches sind.',
'All users' => 'Alle BenutzerInnen',
'Allow access' => 'Zugriff erlauben',
+ 'Allow conversion from sales orders to sales invoices' => 'Umwandlung von Verkaufsaufträgen in Verkaufsrechnungen zulassen',
+ 'Allow conversion from sales quotations to sales invoices' => 'Umwandlung von Verkaufsangeboten in Verkaufsrechnungen zulassen',
+ 'Allow direct creation of new purchase delivery orders' => 'Direktes Anlegen neuer Einkaufslieferscheine zulassen',
+ 'Allow direct creation of new purchase invoices' => 'Direktes Anlegen neuer Einkaufsrechnungen zulassen',
'Allow the following users access to my follow-ups:' => 'Erlaube den folgenden Benutzern Zugriff auf meine Wiedervorlagen:',
'Alternatively you can create a new part which will then be selected.' => 'Sie können auch einen neuen Artikel anlegen, der dann automatisch ausgewählt wird.',
'Amended Advance Turnover Tax Return' => 'Berichtigte Anmeldung',
'Basic Settings for the Requirement Spec Template' => 'Grundeinstellungen der Pflichtenheftvorlage',
'Basic settings' => 'Grundeinstellungen',
'Basic settings actions' => 'Aktionen zu Grundeinstellungen',
+ 'Basis of calculation' => 'Berechnungsgrundlage',
'Batch Printing' => 'Druck',
'Bcc' => 'Bcc',
'Bcc E-mail' => 'BCC (E-Mail)',
'Both' => 'Beide',
'Bottom' => 'Unten',
'Bought' => 'Gekauft',
+ 'Break down by' => 'Aufschlüsseln nach',
'Break up the update and contact a service provider.' => 'Diese Option bricht das Update ab. Bitte kontaktieren Sie Ihren Administrator oder beauftragen einen Dienstleister.',
'Buchungsdatum' => 'Buchungsdatum',
'Buchungsgruppe' => 'Buchungsgruppe',
'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen',
'CSV import: projects' => 'CSV-Import: Projekte',
'CSV import: shipping addresses' => 'CSV-Import: Lieferadressen',
+ 'CTI settings' => 'CTI-Einstellungen',
'Calculate' => 'Berechnen',
+ 'Calling #1 now' => 'Wähle jetzt #1',
'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.',
'Cancel' => 'Abbrechen',
'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren',
'Choose Vendor' => 'Händler wählen',
'Choose a Tax Number' => 'Bitte eine Steuernummer angeben',
'City' => 'Stadt',
+ 'Clear fields' => 'Felder leeren',
'Cleared Balance' => 'abgeschlossen',
'Clearing Tax Received (No 71)' => 'Verrechnung des Erstattungsbetrages erwünscht (Zeile 71)',
'Client' => 'Mandant',
'Conversion to PDF failed: #1' => 'Konvertierung zu PDF schlug fehl: #1',
'Copies' => 'Kopien',
'Copy' => 'Kopieren',
+ 'Copy address from master data' => 'Adresse aus Stammdaten kopieren',
'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3',
'Copy requirement spec' => 'Pflichtenheft kopieren',
'Copy template' => 'Vorlage kopieren',
'Destination warehouse and bin' => 'Ziellager und -lagerplatz',
'Detail view' => 'Detailanzeige',
'Details (one letter abbreviation)' => 'D',
+ 'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration',
'Difference' => 'Differenz',
'Dimensions' => 'Abmessungen',
'Directory' => 'Verzeichnis',
'Ertrag' => 'Ertrag',
'Ertrag prozentual' => 'Ertrag prozentual',
'Escape character' => 'Escape-Zeichen',
- 'Etikett' => '',
+ 'Etikett' => 'Etikett',
'EuR' => 'EuR',
'Everyone can log in.' => 'Alle können sich anmelden.',
'Exact' => 'Genau',
'Existing file on server' => 'Auf dem Server existierende Datei',
'Existing pending follow-ups for this item' => 'Noch nicht erledigte Wiedervorlagen für dieses Dokument',
'Existing profiles' => 'Existierende Profile',
+ 'Exp. bill. date' => 'Vorauss. Abr.datum',
+ 'Exp. netamount' => 'Vorauss. Summe',
'Expected Tax' => 'Erwartete Steuern',
+ 'Expected billing date' => 'Voraussichtliches Abrechnungsdatum',
'Expense' => 'Aufwandskonto',
'Expense Account' => 'Aufwandskonto',
'Expense/Asset' => 'Aufwand/Anlagen',
'If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.' => 'Weichen die Beträge mehr als die "maximale Betragsabweichung" (siehe Einstellungen) ab, so wird diese Position als ungültig markiert.',
'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' => 'Falls angehakt wird der DATEV-Steuerschlüssel bei Buchungen auf dieses Konto nicht beim DATEV-Export mitexportiert, allerdings nur wenn zusätzlich der Konto-Steuerschlüssel vom Buchungs (Hauptbuch) Steuerschlüssel abweicht',
'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' => 'Falls konfiguriert, wird dieses Lager mit Lagerplatz für neu angelegte Waren vorausgewählt.',
+ 'If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.' => 'Falls deaktiviert, so können Einkaufslieferscheine nur durch Umwandlung aus bestehenden Preisanfragen und Lieferantenaufträgen angelegt werden.',
+ 'If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.' => 'Falls deaktiviert, so können Einkaufsrechnungen nur durch Umwandlung aus bestehenden Preisanfragen, Lieferantenaufträgen und Einkaufslieferscheinen angelegt werden.',
+ 'If disabled sales orders cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsangebote nicht direkt in Verkaufsrechnungen umgewandelt werden.',
+ 'If disabled sales quotations cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsauträge nicht direkt in Verkaufsrechnungen umgewandelt werden.',
+ 'If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.' => 'Wenn eingeschaltet, so werden in Verkaufsbelegen nur diejenigen Projekte zur Auswahl angeboten, die dem aktuell ausgewählten Kunden zugewiesen wurden.',
+ 'If enabled purchase and sales records cannot be saved if no transaction description has been entered.' => 'Wenn angeschaltet, so können Einkaufs- und Verkaufsbelege nicht gespeichert werden, solange keine Vorgangsbezeichnung eingegeben wurde.',
'If missing then the start date will be used.' => 'Falls es fehlt, so wird die erste Rechnung für das Startdatum erzeugt.',
'If the article type is set to \'mixed\' then a column called \'type\' must be present.' => 'Falls der Artikeltyp auf \'gemischt\' gestellt wird, muss eine Spalte namens \'type\' vorhanden sein.',
'If the automatic creation of invoices for fees and interest is switched on for a dunning level then the following accounts will be used for the invoice.' => 'Wenn das automatische Erstellen einer Rechnung über Mahngebühren und Zinsen für ein Mahnlevel aktiviert ist, so werden die folgenden Konten für die Rechnung benutzt.',
'Interest' => 'Zinsen',
'Interest Rate' => 'Zinssatz',
'Internal Notes' => 'Interne Bemerkungen',
+ 'Internal Phone List' => 'Interne Telefonliste',
'Internal comment' => 'Interne Bemerkungen',
'Internet' => 'Internet',
'Introduction of clients' => 'Einführung von Mandanten',
'Link to' => 'Verknüpfen mit',
'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:',
'Linked Records' => 'Verknüpfte Belege',
+ 'Liquidity projection' => 'Liquiditätsübersicht',
'List Accounts' => 'Konten anzeigen',
'List Languages' => 'Sprachen anzeigen',
'List Price' => 'Listenpreis',
'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.',
'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.',
'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.',
+ 'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.',
'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.',
'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.',
'No vendor has been selected yet.' => 'Es wurde noch kein Lieferant ausgewählt.',
'No warehouse has been created yet or the quantity of the bins is not configured yet.' => 'Es wurde noch kein Lager angelegt, bzw. die dazugehörigen Lagerplätze sind noch nicht konfiguriert.',
'No.' => 'Position',
+ 'No/individual shipping address' => 'Keine/individuelle Lieferadresse',
'None' => 'Kein',
'Normal users cannot log in.' => 'Normale Benutzer können sich nicht anmelden.',
'Normalize Customer / Vendor names' => 'Normalisierung Kunden- / Lieferantennamen',
'Number of bins' => 'Anzahl Lagerplätze',
'Number of copies' => 'Anzahl Kopien',
'Number of entries changed: #1' => 'Anzahl geänderter Einträge: #1',
+ 'Number of months' => 'Anzahl Monate',
'Number of new bins' => 'Anzahl neuer Lagerplätze',
'Number pages' => 'Seiten nummerieren',
'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => 'Zahlenvariablen: Mit \'PRECISION=n\' erzwingt man, dass Zahlen mit n Nachkommastellen formatiert werden.',
'Only Warnings and Errors' => 'Nur Warnungen und Fehler',
'Only due follow-ups' => 'Nur fällige Wiedervorlagen',
'Only groups that have been configured for the client the user logs in to will be considered.' => 'Allerdings werden nur diejenigen Gruppen herangezogen, die für den Mandanten konfiguriert sind.',
+ 'Only list customer\'s projects in sales records' => 'Nur Projekte des Kunden in Verkaufsbelegen anzeigen',
'Only shown in item mode' => 'werden nur im Artikelmodus angezeigt',
'Oops. No valid action found to dispatch. Please report this case to the kivitendo team.' => 'Ups. Es wurde keine gültige Funktion zum Aufrufen gefunden. Bitte berichten Sie diesen Fall den kivitendo-Entwicklern.',
'Open' => 'Offen',
'Order Number missing!' => 'Auftragsnummer fehlt!',
'Order amount' => 'Auftragswert',
'Order deleted!' => 'Auftrag gelöscht!',
+ 'Order probability' => 'Auftragswahrscheinlichkeit',
+ 'Order probability & expected billing date' => 'Auftragswahrscheinlichkeit & vorrauss. Abrechnungsdatum',
'Order/Item row name' => 'Name der Auftrag-/Positions-Zeilen',
'OrderItem' => 'Position',
'Ordered' => 'Von Kunden bestellt',
'PRINTED' => 'Gedruckt',
'Package name' => 'Paketname',
'Packing Lists' => 'Lieferschein',
- 'Packliste' => '',
+ 'Packliste' => 'Packliste',
'Page' => 'Seite',
'Page #1/#2' => 'Seite #1/#2',
'Paid' => 'bezahlt',
'Part Number' => 'Artikelnummer',
'Part Number missing!' => 'Artikelnummer fehlt!',
'Part picker' => 'Artikelauswahl',
+ 'Partial invoices' => 'Teilrechnungen',
'Partnumber' => 'Artikelnummer',
'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.',
'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!',
'Parts must have an entry type.' => 'Waren müssen eine Buchungsgruppe haben.',
'Parts with existing part numbers' => 'Artikel mit existierender Artikelnummer',
'Parts, services and assemblies' => 'Waren, Dienstleistungen und Erzeugnisse',
- 'Partsedit' => '',
+ 'Partsedit' => 'Wareneditor',
'Partsgroup (database ID)' => 'Warengruppe (Datenbank-ID)',
'Partsgroup (name)' => 'Warengruppe (Name)',
'Password' => 'Passwort',
'Person' => 'Person',
'Personal settings' => 'Persönliche Einstellungen',
'Phone' => 'Telefon',
+ 'Phone extension' => 'Durchwahl',
+ 'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration',
+ 'Phone password' => 'Telefonpasswort',
+ 'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration',
'Phone1' => 'Telefon 1 ',
'Phone2' => 'Telefon 2',
'Pick List' => 'Sammelliste',
'Purchase net amount' => 'EK-Betrag',
'Purchase price' => 'EK-Preis',
'Purchase price total' => 'EK-Betrag',
+ 'Purchasing & Sales' => 'Einkauf & Verkauf',
'Purpose' => 'Verwendungszweck',
'Qty' => 'Menge',
'Qty according to delivery order' => 'Menge laut Lieferschein',
'Representative' => 'Vertreter',
'Representative for Customer' => 'Vertreter für Kunden',
'Reqdate' => 'Liefertermin',
+ 'Reqdate not set or before current month' => 'Lieferdatum nicht gesetzt oder vor aktuellem Monat',
'Request Quotations' => 'Preisanfragen',
'Request for Quotation' => 'Anfrage',
'Request for Quotation Number' => 'Anfragenummer',
'Requested execution date from' => 'Gewünschtes Ausführungsdatum von',
'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis',
'Requests for Quotation' => 'Preisanfragen',
+ 'Require a transaction description in purchase and sales records' => 'Vorgangsbezeichnung in Einkaufs- und Verkaufsbelegen erzwingen',
'Required by' => 'Lieferdatum',
'Requirement Spec Status' => 'Pflichtenheftstatus',
'Requirement Spec Statuses' => 'Pflichtenheftstatus',
'The access rights have been saved.' => 'Die Zugriffsrechte wurden gespeichert.',
'The account 3804 already exists, the update will be skipped.' => 'Das Konto 3804 existiert schon, das Update wird übersprungen.',
'The account 3804 will not be added automatically.' => 'Das Konto 3804 wird nicht automatisch hinzugefügt.',
+ 'The action is missing or invalid.' => 'Die action fehlt, oder sie ist ungültig.',
'The action you\'ve chosen has not been executed because the document does not contain any item yet.' => 'Die von Ihnen ausgewählte Aktion wurde nicht ausgeführt, weil der Beleg noch keine Positionen enthält.',
'The administration area is always accessible.' => 'Der Administrationsbereich ist immer zugänglich.',
'The application "#1" was not found on the system.' => 'Die Anwendung "#1" wurde auf dem System nicht gefunden.',
'The project type has been deleted.' => 'Der Projekttyp wurde gelöscht.',
'The project type has been saved.' => 'Der Projekttyp wurde gespeichert.',
'The project type is in use and cannot be deleted.' => 'Der Projekttyp wird verwendet und kann nicht gelöscht werden.',
+ 'The recipient, subject or body is missing.' => 'Der Empfäger, der Betreff oder der Text ist leer.',
'The required information consists of the IBAN and the BIC.' => 'Die benötigten Informationen bestehen aus der IBAN und der BIC.',
'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => 'Die benötigten Informationen bestehen aus IBAN, BIC, Mandanten-ID und dem Unterschriftsdatum des Mandates.',
'The requirement spec has been deleted.' => 'Das Pflichtenheft wurde gelöscht.',
'not yet executed' => 'Noch nicht ausgeführt',
'number' => 'Nummer',
'oe.pl::search called with unknown type' => 'oe.pl::search mit unbekanntem Typ aufgerufen',
+ 'old' => 'alt',
'on the same day' => 'am selben Tag',
'one-time execution' => 'einmalige Ausführung',
'only OB Transactions' => 'nur EB-Buchungen',
'prev' => 'zurück',
'print' => 'drucken',
'proforma' => 'Proforma',
+ 'prospective' => 'zukünftig',
'purchase_delivery_order_list' => 'lieferscheinliste_einkauf',
'purchase_order' => 'Auftrag',
'purchase_order_list' => 'lieferantenauftragsliste',
\n=<br>
[Template/LaTeX]
-order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad ➔ → ←
+order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad \xa0 ➔ → ←
\\=\\textbackslash\s
<pagebreak>=
"=''
➔=$\\rightarrow$
→=$\\rightarrow$
←=$\\leftarrow$
+\xa0=~
[Template/OpenDocument]
order=& < > " ' \x80 \n \r
[AP--Add Delivery Note]
ACCESS=purchase_delivery_order_edit
+INSTANCE_CONF=allow_new_purchase_delivery_order
module=do.pl
action=add
type=purchase_delivery_order
[AP--Add Vendor Invoice]
ACCESS=vendor_invoice_edit
+INSTANCE_CONF=allow_new_purchase_invoice
module=ir.pl
action=add
type=invoice
module=controller.pl
action=FinancialOverview/list
+[Reports--Liquidity projection]
+ACCESS=report
+module=controller.pl
+action=LiquidityProjection/show
[Batch Printing]
ACCESS=batch_printing
module=am.pl
action=config
+[Program--Internal Phone List]
+module=controller.pl
+action=CTI/list_internal_extensions
+
[Program--Version]
module=login.pl
action=company_logo
+++ /dev/null
-package Term::ReadLine::Perl::Bind;
-### From http://www.perlmonks.org/?node_id=751611
-### Posted by repellant (http://www.perlmonks.org/?node_id=665462)
-
-### Set readline bindkeys for common terminals
-
-use warnings;
-use strict;
-
-BEGIN {
- require Exporter;
- *import = \&Exporter::import; # just inherit import() only
-
- our $VERSION = 1.001;
- our @EXPORT_OK = qw(rl_bind_action $action2key $key2codes);
-}
-
-use Term::ReadLine;
-
-# http://cpansearch.perl.org/src/ILYAZ/Term-ReadLine-Perl-1.0302/ReadLine
-my $got_rl_perl;
-
-BEGIN {
- $got_rl_perl = eval {
- require Term::ReadLine::Perl;
- require Term::ReadLine::readline;
- };
-}
-
-# bindkey actions for terminals
-our $action2key = {
- Complete => "Tab",
- PossibleCompletions => "C-d",
- QuotedInsert => "C-v",
-
- ToggleInsertMode => "Insert",
- DeleteChar => "Del",
- UpcaseWord => "PageUp",
- DownCaseWord => "PageDown",
- BeginningOfLine => "Home",
- EndOfLine => "End",
-
- ReverseSearchHistory => "C-Up",
- ForwardSearchHistory => "C-Down",
- ForwardWord => "C-Right",
- BackwardWord => "C-Left",
-
- HistorySearchBackward => "S-Up",
- HistorySearchForward => "S-Down",
- KillWord => "S-Right",
- BackwardKillWord => "S-Left",
-
- Yank => "A-Down", # paste
- KillLine => "A-Right",
- BackwardKillLine => "A-Left",
-};
-
-our $key2codes = {
- "Tab" => [ "TAB", ],
- "C-d" => [ "C-d", ],
- "C-v" => [ "C-v", ],
-
- "Insert" => [ qq("\e[2~"), qq("\e[2z"), qq("\e[L"), ],
- "Del" => [ qq("\e[3~"), ],
- "PageUp" => [ qq("\e[5~"), qq("\e[5z"), qq("\e[I"), ],
- "PageDown" => [ qq("\e[6~"), qq("\e[6z"), qq("\e[G"), ],
- "Home" => [ qq("\e[7~"), qq("\e[1~"), qq("\e[H"), ],
- "End" => [ qq("\e[8~"), qq("\e[4~"), qq("\e[F"), ],
-
- "C-Up" => [ qq("\eOa"), qq("\eOA"), qq("\e[1;5A"), ],
- "C-Down" => [ qq("\eOb"), qq("\eOB"), qq("\e[1;5B"), ],
- "C-Right" => [ qq("\eOc"), qq("\eOC"), qq("\e[1;5C"), ],
- "C-Left" => [ qq("\eOd"), qq("\eOD"), qq("\e[1;5D"), ],
-
- "S-Up" => [ qq("\e[a"), qq("\e[1;2A"), ],
- "S-Down" => [ qq("\e[b"), qq("\e[1;2B"), ],
- "S-Right" => [ qq("\e[c"), qq("\e[1;2C"), ],
- "S-Left" => [ qq("\e[d"), qq("\e[1;2D"), ],
-
- "A-Down" => [ qq("\e\e[B"), qq("\e[1;3B"), ],
- "A-Right" => [ qq("\e\e[C"), qq("\e[1;3C"), ],
- "A-Left" => [ qq("\e\e[D"), qq("\e[1;3D"), ],
-};
-
-# warn if any keycode is clobbered
-our $debug = 0;
-
-# check ref type
-sub _is_array { ref($_[0]) && eval { @{ $_[0] } or 1 } }
-sub _is_hash { ref($_[0]) && eval { %{ $_[0] } or 1 } }
-
-# set bindkey actions for each terminal
-my %code2action;
-
-sub rl_bind_action {
- if ($got_rl_perl)
- {
- my $a2k = shift();
- return () unless _is_hash($a2k);
-
- while (my ($action, $bindkey) = each %{ $a2k })
- {
- # use default keycodes if none provided
- my @keycodes = @_ ? @_ : $key2codes;
-
- for my $k2c (@keycodes)
- {
- next unless _is_hash($k2c);
-
- my $codes = $k2c->{$bindkey};
- next unless defined($codes);
- $codes = [ $codes ] unless _is_array($codes);
-
- for my $code (@{ $codes })
- {
- if ($debug && $code2action{$code})
- {
- my $hexcode = $code;
- $hexcode =~ s/^"(.*)"$/$1/;
- $hexcode = join(" ", map { uc } unpack("(H2)*", $hexcode));
-
- warn <<"EOT";
-rl_bind_action(): re-binding keycode [ $hexcode ] from '$code2action{$code}' to '$action'
-EOT
- }
-
- readline::rl_bind($code, $action);
- $code2action{$code} = $action;
- }
- }
- }
- }
- else
- {
- warn <<"EOT";
-rl_bind_action(): Term::ReadLine::Perl is not available. No bindkeys were set.
-EOT
- }
-
- return $got_rl_perl;
-}
-
-# default bind
-rl_bind_action($action2key);
-
-# bind Delete key for 'xterm'
-if ($got_rl_perl && defined($ENV{TERM}) && $ENV{TERM} =~ /xterm/)
-{
- rl_bind_action($action2key, +{ "Del" => qq("\x7F") });
-}
-
-'Term::ReadLine::Perl::Bind';
-
use File::Slurp;
use Getopt::Long;
use Pod::Usage;
-use Term::ReadLine::Perl::Bind; # use sane key binding for rxvt users
use SL::LxOfficeConf;
SL::LxOfficeConf->read;
pp DATA - zeigt die Datenstruktur mit Data::Dumper an.
quit - beendet die Konsole
+ part - shortcuts auf die jeweilige SL::DB::{...}::find_by
+ customer, vendor
+ order, invoice
+
EOL
# load 'module' - läd das angegebene Modul, d.h. bin/mozilla/module.pl und SL/Module.pm.
}
}
}
+sub part {
+ require SL::DB::Part;
+ SL::DB::Manager::Part->find_by(@_)
+}
+
+sub order {
+ require SL::DB::Order;
+ SL::DB::Manager::Order->find_by(@_)
+}
+
+sub invoice {
+ require SL::DB::Invoice;
+ SL::DB::Manager::Invoice->find_by(@_)
+}
+
+sub customer {
+ require SL::DB::Customer;
+ SL::DB::Manager::Customer->find_by(@_)
+}
+
+sub vendor {
+ require SL::DB::Vendor;
+ SL::DB::Manager::Vendor->find_by(@_)
+}
+
+
1;
__END__
our @lost = ();
my %ignore_unused_templates = (
- map { $_ => 1 } qw(ct/testpage.html generic/autocomplete.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js)
+ map { $_ => 1 } qw(ct/testpage.html generic/autocomplete.html oe/periodic_invoices_email.txt part/testpage.html t/render.html t/render.js task_server/failure_notification_email.txt)
);
my (%referenced_html_files, %locale, %htmllocale, %alllocales, %cached, %submit, %jslocale);
my %config;
+# Maps column names in tables to foreign key relationship names. For
+# example:
+#
+# »follow_up_access« contains a column named »who«. Rose normally
+# names the resulting relationship after the class the target table
+# uses. In this case the target table is »employee« and the
+# corresponding class SL::DB::Employee. The resulting relationship
+# would be named »employee«.
+#
+# In order to rename this relationship we have to map »who« to
+# e.g. »granted_by«:
+# follow_up_access => { who => 'granted_by' },
+
our %foreign_key_name_map = (
KIVITENDO => {
- oe => { payment => 'payment_terms', },
- ar => { payment => 'payment_terms', },
- ap => { payment => 'payment_terms', },
+ oe => { payment_id => 'payment_terms', },
+ ar => { payment_id => 'payment_terms', },
+ ap => { payment_id => 'payment_terms', },
- orderitems => { parts => 'part', trans => 'order', },
- delivery_order_items => { parts => 'part' },
- invoice => { parts => 'part' },
- follow_ups => { 'employee_obj' => 'created_for' },
+ orderitems => { parts_id => 'part', trans_id => 'order', },
+ delivery_order_items => { parts_id => 'part' },
+ invoice => { parts_id => 'part' },
+ follow_ups => { created_for_user => 'created_for', created_by => 'created_by', },
+ follow_up_access => { who => 'with_access', what => 'to_follow_ups_by', },
- periodic_invoices_configs => { oe => 'order' },
+ periodic_invoices_configs => { oe_id => 'order' },
},
);
}
}
+sub fix_relationship_names {
+ my ($domain, $table, $fkey_text) = @_;
+
+ if ($fkey_text !~ m/key_columns \s+ => \s+ \{ \s+ ['"]? ( [^'"\s]+ ) /x) {
+ die "fix_relationship_names: could not extract the key column for domain/table $domain/$table; foreign key definition text:\n${fkey_text}\n";
+ }
+
+ my $column_name = $1;
+ my %changes = map { %{$_} } grep { $_ } ($foreign_key_name_map{$domain}->{ALL}, $foreign_key_name_map{$domain}->{$table});
+
+ if (my $desired_name = $changes{$column_name}) {
+ $fkey_text =~ s/^ \s\s [^\s]+ \b/ ${desired_name}/msx;
+ }
+
+ return $fkey_text;
+}
+
sub process_table {
my ($domain, $table, $package) = @_;
my $schema = '';
$foreign_key_definition =~ s/::AUTO::/::/g;
if ($foreign_key_definition && ($definition =~ /\Q$foreign_key_definition\E/)) {
+ # These positions refer to the whole setup call, not just the
+ # parameters/actual relationship definitions.
my ($start, $end) = ($-[0], $+[0]);
- my %changes = map { %{$_} } grep { $_ } ($foreign_key_name_map{$domain}->{ALL}, $foreign_key_name_map{$domain}->{$table});
- while (my ($auto_generated_name, $desired_name) = each %changes) {
- $foreign_key_definition =~ s/^ \s \s ${auto_generated_name} \b/ ${desired_name}/msx;
- }
+ # Match the function parameters = the actual relationship
+ # definitions
+ next unless $foreign_key_definition =~ m/\(\n(.+)\n\)/s;
- # Sort foreign key definitions alphabetically
- if ($foreign_key_definition =~ m/\(\n(.+)\n\)/s) {
- my ($list_start, $list_end) = ($-[0], $+[0]);
- my @foreign_keys = split m/\n\n/m, $1;
- my $sorted_foreign_keys = "(\n" . join("\n\n", sort @foreign_keys) . "\n)";
+ my ($list_start, $list_end) = ($-[0], $+[0]);
- substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys;;
- }
+ # Split the whole chunk on double new lines. The resulting
+ # elements are one relationship each. Then fix the relationship
+ # names and sort them by their new names.
+ my @new_foreign_keys = sort map { fix_relationship_names($domain, $table, $_) } split m/\n\n/m, $1;
+
+ # Replace the function parameters = the actual relationship
+ # definitions with the new ones.
+ my $sorted_foreign_keys = "(\n" . join("\n\n", @new_foreign_keys) . "\n)";
+ substr $foreign_key_definition, $list_start, $list_end - $list_start, $sorted_foreign_keys;
- substr($definition, $start, $end - $start) = $foreign_key_definition;
+ # Replace the whole setup call in the auto-generated output with
+ # our new version.
+ substr $definition, $start, $end - $start, $foreign_key_definition;
}
$definition =~ s/(meta->table.*)\n/$1\n$schema_str/m if $schema;
die "cannot find locale for user $login" unless $::locale = Locale->new('de');
}
+sub per_job_initialization {
+ $::locale = Locale->new($::lx_office_conf{system}->{language});
+ $::form = Form->new;
+ $::instance_conf = SL::InstanceConfiguration->new;
+ $::request = SL::Request->new(
+ cgi => CGI->new({}),
+ layout => SL::Layout::None->new,
+ );
+
+ $::auth->restore_session;
+
+ $::form->{login} = $lx_office_conf{task_server}->{login};
+ $::instance_conf->init;
+}
+
sub drop_privileges {
my $user = $lx_office_conf{task_server}->{run_as};
return unless $user;
foreach my $job (@{ $jobs }) {
# Provide fresh global variables in case legacy code modifies
# them somehow.
- $::locale = Locale->new($::lx_office_conf{system}->{language});
- $::form = Form->new;
+ per_job_initialization();
chdir $exe_dir;
--- /dev/null
+-- @tag: column_type_text_instead_of_varchar
+-- @description: Spaltentyp auf Text anstelle von varchar() für diverse Spalten
+-- @depends: release_3_1_0
+
+-- contacts
+ALTER TABLE contacts
+ ALTER COLUMN cp_givenname TYPE TEXT
+ , ALTER COLUMN cp_title TYPE TEXT
+ , ALTER COLUMN cp_name TYPE TEXT
+ , ALTER COLUMN cp_phone1 TYPE TEXT
+ , ALTER COLUMN cp_phone2 TYPE TEXT
+ , ALTER COLUMN cp_position TYPE TEXT
+ ;
+
+-- customer
+ALTER TABLE customer
+ ALTER COLUMN bic TYPE TEXT
+ , ALTER COLUMN city TYPE TEXT
+ , ALTER COLUMN country TYPE TEXT
+ , ALTER COLUMN department_1 TYPE TEXT
+ , ALTER COLUMN department_2 TYPE TEXT
+ , ALTER COLUMN fax TYPE TEXT
+ , ALTER COLUMN iban TYPE TEXT
+ , ALTER COLUMN language TYPE TEXT
+ , ALTER COLUMN street TYPE TEXT
+ , ALTER COLUMN username TYPE TEXT
+ , ALTER COLUMN zipcode TYPE TEXT
+ ;
+
+-- vendor
+ALTER TABLE vendor
+ ALTER COLUMN bic TYPE TEXT
+ , ALTER COLUMN city TYPE TEXT
+ , ALTER COLUMN country TYPE TEXT
+ , ALTER COLUMN department_1 TYPE TEXT
+ , ALTER COLUMN department_2 TYPE TEXT
+ , ALTER COLUMN fax TYPE TEXT
+ , ALTER COLUMN iban TYPE TEXT
+ , ALTER COLUMN street TYPE TEXT
+ , ALTER COLUMN user_password TYPE TEXT
+ , ALTER COLUMN username TYPE TEXT
+ , ALTER COLUMN zipcode TYPE TEXT
+ ;
--- /dev/null
+-- @tag: column_type_text_instead_of_varchar2
+-- @description: Spaltentyp auf Text anstelle von varchar() für diverse Spalten Teil 2
+-- @depends: column_type_text_instead_of_varchar
+
+-- shipto
+ALTER TABLE shipto
+ ALTER COLUMN shiptocity TYPE TEXT
+ , ALTER COLUMN shiptocontact TYPE TEXT
+ , ALTER COLUMN shiptocountry TYPE TEXT
+ , ALTER COLUMN shiptodepartment_1 TYPE TEXT
+ , ALTER COLUMN shiptodepartment_2 TYPE TEXT
+ , ALTER COLUMN shiptofax TYPE TEXT
+ , ALTER COLUMN shiptoname TYPE TEXT
+ , ALTER COLUMN shiptophone TYPE TEXT
+ , ALTER COLUMN shiptostreet TYPE TEXT
+ , ALTER COLUMN shiptozipcode TYPE TEXT
+ ;
--- /dev/null
+-- @tag: column_type_text_instead_of_varchar3
+-- @description: Spaltentyp Text anstelle von varchar() in diversen Tabellen Teil 3
+-- @depends: column_type_text_instead_of_varchar2
+
+-- vendor
+ALTER TABLE vendor ALTER COLUMN language TYPE TEXT;
--- /dev/null
+-- @tag: defaults_only_customer_projects_in_sales
+-- @description: Mandantenkonfiguration: in Verkaufsbelegen nur Projekte des ausgewählten Kunden anbieten
+-- @depends: release_3_1_0
+ALTER TABLE defaults ADD COLUMN customer_projects_only_in_sales BOOLEAN;
+UPDATE defaults SET customer_projects_only_in_sales = FALSE;
+ALTER TABLE defaults
+ ALTER COLUMN customer_projects_only_in_sales SET DEFAULT FALSE,
+ ALTER COLUMN customer_projects_only_in_sales SET NOT NULL;
--- /dev/null
+-- @tag: defaults_require_transaction_description
+-- @description: Mandantenkonfiguration: optional Existenz der Vorgangsbezeichnung erzwingen
+-- @depends: release_3_1_0
+ALTER TABLE defaults ADD COLUMN require_transaction_description_ps BOOLEAN;
+UPDATE defaults SET require_transaction_description_ps = FALSE;
+
+ALTER TABLE defaults
+ ALTER COLUMN require_transaction_description_ps SET DEFAULT FALSE,
+ ALTER COLUMN require_transaction_description_ps SET NOT NULL;
--- /dev/null
+-- @tag: defaults_sales_purchase_process_limitations
+-- @description: Mandantenkonfiguration: Einschränkungen, welche Aktionen im Einkaufs-/Verkaufsprozess erlaubt sind
+-- @depends: release_3_1_0
+ALTER TABLE defaults
+ ADD COLUMN allow_sales_invoice_from_sales_quotation BOOLEAN,
+ ADD COLUMN allow_sales_invoice_from_sales_order BOOLEAN,
+ ADD COLUMN allow_new_purchase_delivery_order BOOLEAN,
+ ADD COLUMN allow_new_purchase_invoice BOOLEAN;
+
+UPDATE defaults
+SET allow_sales_invoice_from_sales_quotation = TRUE,
+ allow_sales_invoice_from_sales_order = TRUE,
+ allow_new_purchase_delivery_order = TRUE,
+ allow_new_purchase_invoice = TRUE;
+
+ALTER TABLE defaults
+ ALTER COLUMN allow_sales_invoice_from_sales_quotation SET DEFAULT TRUE,
+ ALTER COLUMN allow_sales_invoice_from_sales_quotation SET NOT NULL,
+
+ ALTER COLUMN allow_sales_invoice_from_sales_order SET DEFAULT TRUE,
+ ALTER COLUMN allow_sales_invoice_from_sales_order SET NOT NULL,
+
+ ALTER COLUMN allow_new_purchase_delivery_order SET DEFAULT TRUE,
+ ALTER COLUMN allow_new_purchase_delivery_order SET NOT NULL,
+
+ ALTER COLUMN allow_new_purchase_invoice SET DEFAULT TRUE,
+ ALTER COLUMN allow_new_purchase_invoice SET NOT NULL;
--- /dev/null
+-- @tag: delete_cvars_on_trans_deletion
+-- @description: Einträge in benutzerdefinierten Variablen löschen, deren Bezugsbelege gelöscht wurde
+-- @depends: release_3_1_0
+
+-- 1. Alle benutzerdefinierten Variablen löschen, für die es keine
+-- Einträge in den dazugehörigen Tabellen mehr gibt.
+
+-- 1.1. Alle CVars für Artikel selber (sub_module ist leer):
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
+ AND (COALESCE(sub_module, '') = '')
+ AND (trans_id NOT IN (SELECT id FROM parts));
+
+-- 1.2. Alle CVars für Angebote/Aufträge, Lieferscheine, Rechnungen
+-- (sub_module gesetzt):
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
+ AND (sub_module = 'orderitems')
+ AND (trans_id NOT IN (SELECT id FROM orderitems));
+
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
+ AND (sub_module = 'delivery_order_items')
+ AND (trans_id NOT IN (SELECT id FROM delivery_order_items));
+
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
+ AND (sub_module = 'invoice')
+ AND (trans_id NOT IN (SELECT id FROM invoice));
+
+-- 1.3. Alle CVars für Kunden/Lieferanten:
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'CT'))
+ AND (trans_id NOT IN (SELECT id FROM customer UNION SELECT id FROM vendor));
+
+-- 1.4. Alle CVars für Ansprechpersonen:
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'Contacts'))
+ AND (trans_id NOT IN (SELECT cp_id FROM contacts));
+
+-- 1.5. Alle CVars für Projekte:
+DELETE FROM custom_variables
+WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'Projects'))
+ AND (trans_id NOT IN (SELECT id FROM project));
+
+-- 2. Triggerfunktionen erstellen, die die benutzerdefinierten
+-- Variablen löschen.
+
+-- 2.1. Parametrisierte Backend-Funktion zum Löschen:
+CREATE OR REPLACE FUNCTION delete_custom_variables_with_sub_module(config_module TEXT, cvar_sub_module TEXT, old_id INTEGER)
+RETURNS BOOLEAN AS $$
+ BEGIN
+ DELETE FROM custom_variables
+ WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = config_module))
+ AND (COALESCE(sub_module, '') = cvar_sub_module)
+ AND (trans_id = old_id);
+
+ RETURN TRUE;
+ END;
+$$ LANGUAGE plpgsql;
+
+-- 2.2. Nun die Funktionen, die als Trigger aufgerufen wird und die
+-- entscheidet, wie genau zu löschen ist:
+CREATE OR REPLACE FUNCTION delete_custom_variables_trigger()
+RETURNS TRIGGER AS $$
+ BEGIN
+ IF (TG_TABLE_NAME IN ('orderitems', 'delivery_order_items', 'invoice')) THEN
+ PERFORM delete_custom_variables_with_sub_module('IC', TG_TABLE_NAME, old.id);
+ END IF;
+
+ IF (TG_TABLE_NAME = 'parts') THEN
+ PERFORM delete_custom_variables_with_sub_module('IC', '', old.id);
+ END IF;
+
+ IF (TG_TABLE_NAME IN ('customer', 'vendor')) THEN
+ PERFORM delete_custom_variables_with_sub_module('CT', '', old.id);
+ END IF;
+
+ IF (TG_TABLE_NAME = 'contacts') THEN
+ PERFORM delete_custom_variables_with_sub_module('Contacts', '', old.id);
+ END IF;
+
+ IF (TG_TABLE_NAME = 'project') THEN
+ PERFORM delete_custom_variables_with_sub_module('Projects', '', old.id);
+ END IF;
+
+ RETURN old;
+ END;
+$$ LANGUAGE plpgsql;
+
+-- 3. Die eigentlichen Trigger erstellen:
+
+-- 3.1. orderitems
+DROP TRIGGER IF EXISTS orderitems_delete_custom_variables_after_deletion ON orderitems;
+
+CREATE TRIGGER orderitems_delete_custom_variables_after_deletion
+AFTER DELETE ON orderitems
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.2. delivery_order_items
+DROP TRIGGER IF EXISTS delivery_order_items_delete_custom_variables_after_deletion ON delivery_order_items;
+
+CREATE TRIGGER delivery_order_items_delete_custom_variables_after_deletion
+AFTER DELETE ON delivery_order_items
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.3. invoice
+DROP TRIGGER IF EXISTS invoice_delete_custom_variables_after_deletion ON invoice;
+
+CREATE TRIGGER invoice_delete_custom_variables_after_deletion
+AFTER DELETE ON invoice
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.4. parts
+DROP TRIGGER IF EXISTS parts_delete_custom_variables_after_deletion ON parts;
+
+CREATE TRIGGER parts_delete_custom_variables_after_deletion
+AFTER DELETE ON parts
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.5. customer
+DROP TRIGGER IF EXISTS customer_delete_custom_variables_after_deletion ON customer;
+
+CREATE TRIGGER customer_delete_custom_variables_after_deletion
+AFTER DELETE ON customer
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.6. vendor
+DROP TRIGGER IF EXISTS vendor_delete_custom_variables_after_deletion ON vendor;
+
+CREATE TRIGGER vendor_delete_custom_variables_after_deletion
+AFTER DELETE ON vendor
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.7. contacts
+DROP TRIGGER IF EXISTS contacts_delete_custom_variables_after_deletion ON contacts;
+
+CREATE TRIGGER contacts_delete_custom_variables_after_deletion
+AFTER DELETE ON contacts
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
+
+-- 3.8. project
+DROP TRIGGER IF EXISTS project_delete_custom_variables_after_deletion ON project;
+
+CREATE TRIGGER project_delete_custom_variables_after_deletion
+AFTER DELETE ON project
+FOR EACH ROW EXECUTE PROCEDURE delete_custom_variables_trigger();
--- /dev/null
+-- @tag: sales_quotation_order_probability_expected_billing_date
+-- @charset: utf-8
+-- @description: Weitere Felder im Angebot: Angebotswahrscheinlichkeit, voraussichtliches Abrechnungsdatum
+ALTER TABLE oe
+ ADD COLUMN order_probability INTEGER,
+ ADD COLUMN expected_billing_date DATE;
+
+UPDATE oe SET order_probability = 0;
+
+ALTER TABLE oe
+ ALTER COLUMN order_probability SET DEFAULT 0,
+ ALTER COLUMN order_probability SET NOT NULL;
--- /dev/null
+use Test::More tests => 9;
+
+use strict;
+use lib 't';
+use utf8;
+
+use_ok 'SL::CTI';
+
+{
+ no warnings 'once';
+ $::lx_office_conf{cti}->{international_dialing_prefix} = '00';
+}
+
+is SL::CTI->call_link(number => '0371 5347 620'), 'controller.pl?action=CTI/call&number=03715347620';
+is SL::CTI->call_link(number => '0049(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222';
+is SL::CTI->call_link(number => '+49(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222';
+is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'controller.pl?action=CTI/call&number=0049408090640';
+
+is SL::CTI->call_link(number => '0371 5347 620', internal => 1), 'controller.pl?action=CTI/call&number=03715347620&internal=1';
+is SL::CTI->call_link(number => '0049(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1';
+is SL::CTI->call_link(number => '+49(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1';
+is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1';
--- /dev/null
+use Test::More tests => 5;
+
+use strict;
+use lib 't';
+use utf8;
+
+use_ok 'SL::CTI';
+
+{
+ no warnings 'once';
+ $::lx_office_conf{cti}->{international_dialing_prefix} = '00';
+}
+
+is SL::CTI->sanitize_number(number => '0371 5347 620'), '03715347620';
+is SL::CTI->sanitize_number(number => '0049(0)421-22232 22'), '0049421-2223222';
+is SL::CTI->sanitize_number(number => '+49(0)421-22232 22'), '0049421-2223222';
+is SL::CTI->sanitize_number(number => 'Tel: +49 40 809064 0'), '0049408090640';
}, "${title}: calculated data");
}
+sub test_default_invoice_three_items_sellprice_rounding_discount() {
+ reset_state();
+
+ my $item1 = new_item(qty => 1, sellprice => 5.55, discount => .05);
+ my $item2 = new_item(qty => 1, sellprice => 5.50, discount => .05);
+ my $item3 = new_item(qty => 1, sellprice => 5.00, discount => .05);
+ my $invoice = new_invoice(
+ taxincluded => 0,
+ invoiceitems => [ $item1, $item2, $item3 ],
+ );
+
+ # this is how price_tax_calculator is implemented. It differs from
+ # the way sales_order / invoice - forms are calculating:
+ # linetotal = sellprice 5.55 * qty 1 * (1 - 0.05) = 5.2725; rounded 5.27
+ # linetotal = sellprice 5.50 * qty 1 * (1 - 0.05) = 5.225 rounded 5.23
+ # linetotal = sellprice 5.00 * qty 1 * (1 - 0.05) = 4.75; rounded 4.75
+ # ...
+
+ # item 1:
+ # discount = sellprice 5.55 * discount (0.05) = 0.2775; rounded 0.28
+ # sellprice = sellprice 5.55 - discount 0.28 = 5.27; rounded 5.27
+ # linetotal = sellprice 5.27 * qty 1 = 5.27; rounded 5.27
+ # 19%(5.27) = 1.0013; rounded = 1.00
+ # total rounded = 6.27
+
+ # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
+ # line marge_total = 3.34
+ # line marge_percent = 63.3776091081594
+
+ # item 2:
+ # discount = sellprice 5.50 * discount 0.05 = 0.275; rounded 0.28
+ # sellprice = sellprice 5.50 - discount 0.28 = 5.22; rounded 5.22
+ # linetotal = sellprice 5.22 * qty 1 = 5.22; rounded 5.22
+ # 19%(5.22) = 0.9918; rounded = 0.99
+ # total rounded = 6.21
+
+ # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
+ # line marge_total = 5.22 - 1.93 = 3.29
+ # line marge_percent = 3.29/5.22 = 0.630268199233716
+
+ # item 3:
+ # discount = sellprice 5.00 * discount 0.25 = 0.25; rounded 0.25
+ # sellprice = sellprice 5.00 - discount 0.25 = 4.75; rounded 4.75
+ # linetotal = sellprice 4.75 * qty 1 = 4.75; rounded 4.75
+ # 19%(4.75) = 0.9025; rounded = 0.90
+ # total rounded = 5.65
+
+ # lastcost 1.93 * qty 1 = 1.93; rounded 1.93
+ # line marge_total = 2.82
+ # line marge_percent = 59.3684210526316
+
+ my $title = 'default invoice, three items, sellprice, rounding, discount';
+ my %data = $invoice->calculate_prices_and_taxes;
+
+ is($item1->marge_total, 3.34, "${title}: item1 marge_total");
+ is($item1->marge_percent, 63.3776091081594, "${title}: item1 marge_percent");
+ is($item1->marge_price_factor, 1, "${title}: item1 marge_price_factor");
+
+ is($item2->marge_total, 3.29, "${title}: item2 marge_total");
+ is($item2->marge_percent, 63.0268199233716, "${title}: item2 marge_percent");
+ is($item2->marge_price_factor, 1, "${title}: item2 marge_price_factor");
+
+ is($item3->marge_total, 2.82, "${title}: item3 marge_total");
+ is($item3->marge_percent, 59.3684210526316, "${title}: item3 marge_percent");
+ is($item3->marge_price_factor, 1, "${title}: item3 marge_price_factor");
+
+ is($invoice->netamount, 5.27 + 5.22 + 4.75, "${title}: netamount");
+
+ # 6.27 + 6.21 + 5.65 = 18.13
+ # 1.19*(5.27 + 5.22 + 4.75) = 18.1356; rounded 18.14
+ #is($invoice->amount, 6.27 + 6.21 + 5.65, "${title}: amount");
+ is($invoice->amount, 18.14, "${title}: amount");
+
+ is($invoice->marge_total, 3.34 + 3.29 + 2.82, "${title}: marge_total");
+ is($invoice->marge_percent, 62.007874015748, "${title}: marge_percent");
+
+ is_deeply(\%data, {
+ allocated => {},
+ amounts => {
+ $buchungsgruppe->income_accno_id_0 => {
+ amount => 15.24,
+ tax_id => $tax->id,
+ taxkey => 3,
+ },
+ },
+ amounts_cogs => {},
+ assembly_items => [
+ [], [], [],
+ ],
+ exchangerate => 1,
+ taxes => {
+ $tax->chart_id => 2.9,
+ },
+ }, "${title}: calculated data");
+}
+
Support::TestSetup::login();
test_default_invoice_one_item_19_tax_not_included();
test_default_invoice_two_items_19_7_tax_not_included();
+test_default_invoice_three_items_sellprice_rounding_discount();
done_testing();
#####
$csv = SL::Helper::Csv->new(
- file => \<<EOL,
+ file => \ Encode::encode('utf-8', <<EOL),
description;longdescription;datatype
name;customernumber;datatype
Kaffee;"lecker Kaffee";P
#####
$csv = SL::Helper::Csv->new(
- file => \<<EOL,
+ file => \ Encode::encode('utf-8', <<EOL),
datatype;description;longdescription
name;datatype;customernumber
P;Kaffee;"lecker Kaffee"
--- /dev/null
+use Test::More tests => 50;
+
+use lib 't';
+
+use Data::Dumper;
+
+use DateTime;
+use_ok 'SL::Helper::DateTime';
+
+sub mon { DateTime->new(year => 2014, month => 6, day => 23) }
+sub tue { DateTime->new(year => 2014, month => 6, day => 24) }
+sub wed { DateTime->new(year => 2014, month => 6, day => 25) }
+sub thu { DateTime->new(year => 2014, month => 6, day => 26) }
+sub fri { DateTime->new(year => 2014, month => 6, day => 27) }
+sub sat { DateTime->new(year => 2014, month => 6, day => 28) }
+sub sun { DateTime->new(year => 2014, month => 6, day => 29) }
+
+
+is mon->add_businessdays(days => 5)->day_of_week, 1, "mon + 5 => mon";
+is mon->add_businessdays(days => 12)->day_of_week, 3, "mon + 12 => wed";
+is fri->add_businessdays(days => 2)->day_of_week, 2, "fri + 2 => tue";
+is tue->add_businessdays(days => 9)->day_of_week, 1, "tue + 9 => mon";
+is tue->add_businessdays(days => 8)->day_of_week, 5, "tue + 8 => fri";
+
+# same with 6day week
+is mon->add_businessdays(businessweek => 6, days => 5)->day_of_week, 6, "mon + 5 => sat (6dw)";
+is mon->add_businessdays(businessweek => 6, days => 12)->day_of_week, 1, "mon + 12 => mon (6dw)";
+is fri->add_businessdays(businessweek => 6, days => 2)->day_of_week, 1, "fri + 2 => mon (6dw)";
+is tue->add_businessdays(businessweek => 6, days => 9)->day_of_week, 5, "tue + 9 => fri (6dw)";
+is tue->add_businessdays(businessweek => 6, days => 8)->day_of_week, 4, "tue + 8 => thu (6dw)";
+
+# absolute dates
+
+is mon->add_businessdays(days => 5), mon->add(days => 7), "mon + 5 => mon (date)";
+is mon->add_businessdays(days => 12), mon->add(days => 16), "mon + 12 => wed (date)";
+is fri->add_businessdays(days => 2), fri->add(days => 4), "fri + 2 => tue (date)";
+is tue->add_businessdays(days => 9), tue->add(days => 13), "tue + 9 => mon (date)";
+is tue->add_businessdays(days => 8), tue->add(days => 10), "tue + 8 => fri (date)";
+
+# same with 6day week
+is mon->add_businessdays(businessweek => 6, days => 5), mon->add(days => 5), "mon + 5 => sat (date) (6dw)";
+is mon->add_businessdays(businessweek => 6, days => 12), mon->add(days => 14), "mon + 12 => mon (date) (6dw)";
+is fri->add_businessdays(businessweek => 6, days => 2), fri->add(days => 3), "fri + 2 => mon (date) (6dw)";
+is tue->add_businessdays(businessweek => 6, days => 9), tue->add(days => 10), "tue + 9 => fri (date) (6dw)";
+is tue->add_businessdays(businessweek => 6, days => 8), tue->add(days => 9), "tue + 8 => thu (date) (6dw)";
+
+
+# same with substract
+
+is mon->subtract_businessdays(days => 5)->day_of_week, 1, "mon - 5 => mon";
+is mon->subtract_businessdays(days => 12)->day_of_week, 4, "mon - 12 => thu";
+is fri->subtract_businessdays(days => 2)->day_of_week, 3, "fri - 2 => wed";
+is tue->subtract_businessdays(days => 9)->day_of_week, 3, "tue - 9 => wed";
+is tue->subtract_businessdays(days => 8)->day_of_week, 4, "tue - 8 => thu";
+
+# same with 6day week
+is mon->subtract_businessdays(businessweek => 6, days => 5)->day_of_week, 2, "mon - 5 => tue (6dw)";
+is mon->subtract_businessdays(businessweek => 6, days => 12)->day_of_week, 1, "mon - 12 => mon (6dw)";
+is fri->subtract_businessdays(businessweek => 6, days => 4)->day_of_week, 1, "fri - 4 => mon (6dw)";
+is tue->subtract_businessdays(businessweek => 6, days => 9)->day_of_week, 5, "tue - 9 => fri (6dw)";
+is tue->subtract_businessdays(businessweek => 6, days => 8)->day_of_week, 6, "tue - 8 => sat (6dw)";
+
+# absolute dates
+
+is mon->subtract_businessdays(days => 5), mon->add(days => -7), "mon - 5 => mon (date)";
+is mon->subtract_businessdays(days => 12), mon->add(days => -18), "mon - 12 => thu (date)";
+is fri->subtract_businessdays(days => 2), fri->add(days => -2), "fri - 2 => wed (date)";
+is tue->subtract_businessdays(days => 9), tue->add(days => -13), "tue - 9 => wed (date)";
+is tue->subtract_businessdays(days => 8), tue->add(days => -12), "tue - 8 => thu (date)";
+
+# same with 6day week
+is mon->subtract_businessdays(businessweek => 6, days => 5), mon->add(days => -6), "mon - 5 => tue (date) (6dw)";
+is mon->subtract_businessdays(businessweek => 6, days => 12), mon->add(days => -14), "mon - 12 => mon (date) (6dw)";
+is fri->subtract_businessdays(businessweek => 6, days => 4), fri->add(days => -4), "fri - 4 => mon (date) (6dw)";
+is tue->subtract_businessdays(businessweek => 6, days => 9), tue->add(days => -11), "tue - 9 => fri (date) (6dw)";
+is tue->subtract_businessdays(businessweek => 6, days => 8), tue->add(days => -10), "tue - 8 => sat (date) (6dw)";
+
+# add with negative days?
+is mon->add_businessdays(businessweek => 6, days => -5), mon->add(days => -6), "mon - 5 => tue (date) (6dw)";
+is mon->add_businessdays(businessweek => 6, days => -12), mon->add(days => -14), "mon - 12 => mon (date) (6dw)";
+is fri->add_businessdays(businessweek => 6, days => -4), fri->add(days => -4), "fri - 4 => mon (date) (6dw)";
+is tue->add_businessdays(businessweek => 6, days => -9), tue->add(days => -11), "tue - 9 => fri (date) (6dw)";
+is tue->add_businessdays(businessweek => 6, days => -8), tue->add(days => -10), "tue - 8 => sat (date) (6dw)";
+
+# what if staring date falls into eekend?
+is sun->add_businessdays(days => 1), sun->add(days => 1), "1 day after sun is mon";
+is sat->add_businessdays(days => 1), sat->add(days => 2), "1 day after sut is mon";
+is sun->add_businessdays(days => -1), sun->add(days => -2), "1 day before sun is fri";
+is sat->add_businessdays(days => -1), sat->add(days => -1), "1 day before sut is fri";
</tr>
</table>
+ <h2>[%- LxERP.t8("CTI settings") %]</h2>
+
+ <table>
+ <tr>
+ <th align="right">[% LxERP.t8("Phone extension") %]</th>
+ <td>[% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% LxERP.t8("Phone password") %]</th>
+ <td>[% L.input_tag("user.config_values.phone_password", props.phone_password) %]</td>
+ </tr>
+ </table>
+
<h2>[%- LxERP.t8("Access to clients") %]</h2>
[% IF SELF.all_clients.size %]
<td nowrap>[% 'Subtotal' | $T8 %]</td>
<td align=right><input name="l_globalprojectnumber" class=checkbox type=checkbox value=Y></td>
<td nowrap>[% 'Document Project Number' | $T8 %]</td>
- <td align=right><input name="l_transaction_description" class=checkbox type=checkbox value=Y></td>
+ <td align=right><input name="l_transaction_description" class=checkbox type=checkbox value=Y[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]></td>
<td nowrap>[% 'Transaction description' | $T8 %]</td>
</tr>
<tr>
</tr>
<tr>
<th align="right">[% LxERP.t8('Status') %]</th>
- <td>[% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failed', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %]</td>
+ <td>[% L.select_tag('filter.status:eq_ignore_empty', [ [ '', '' ], [ 'failure', LxERP.t8('failed') ], [ 'success', LxERP.t8('succeeded') ] ], default=filter.status_eq_ignore_empty) %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8('Run at') %] [% LxERP.t8('From Date') %]</th>
</tr>
</tr>
+ <tr><td class="listheading" colspan="4">[% LxERP.t8("Purchasing & Sales") %]</td></tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8('Require a transaction description in purchase and sales records') %]</td>
+ <td>[% L.yes_no_tag('defaults.require_transaction_description_ps', SELF.defaults.require_transaction_description_ps) %]</td>
+ <td>[% LxERP.t8('If enabled purchase and sales records cannot be saved if no transaction description has been entered.') %]</td>
+ </tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8("Only list customer's projects in sales records") %]</td>
+ <td>[% L.yes_no_tag("defaults.customer_projects_only_in_sales", SELF.defaults.customer_projects_only_in_sales) %]</td>
+ <td>[% LxERP.t8("If enabled only those projects that are assigned to the currently selected customer are offered for selection in sales records.") %]</td>
+ </tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8('Allow conversion from sales quotations to sales invoices') %]</td>
+ <td>[% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_quotation', SELF.defaults.allow_sales_invoice_from_sales_quotation) %]</td>
+ <td>[% LxERP.t8('If disabled sales quotations cannot be converted into sales invoices directly.') %]</td>
+ </tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8('Allow conversion from sales orders to sales invoices') %]</td>
+ <td>[% L.yes_no_tag('defaults.allow_sales_invoice_from_sales_order', SELF.defaults.allow_sales_invoice_from_sales_order) %]</td>
+ <td>[% LxERP.t8('If disabled sales orders cannot be converted into sales invoices directly.') %]</td>
+ </tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8('Allow direct creation of new purchase delivery orders') %]</td>
+ <td>[% L.yes_no_tag('defaults.allow_new_purchase_delivery_order', SELF.defaults.allow_new_purchase_delivery_order) %]</td>
+ <td>[% LxERP.t8('If disabled purchase delivery orders can only be created by conversion from existing requests for quotations and purchase orders.') %]</td>
+ </tr>
+
+ <tr>
+ <td align="right">[% LxERP.t8('Allow direct creation of new purchase invoices') %]</td>
+ <td>[% L.yes_no_tag('defaults.allow_new_purchase_invoice', SELF.defaults.allow_new_purchase_invoice) %]</td>
+ <td>[% LxERP.t8('If disabled purchase invoices can only be created by conversion from existing requests for quotations, purchase orders and purchase delivery orders.') %]</td>
+ </tr>
+
<tr><td class="listheading" colspan="4">[% LxERP.t8("Requirement Specs") %]</td></tr>
<tr>
<input name="l_discount" id="l_discount" type="checkbox" class="checkbox" value="Y">
<label for="l_discount">[% 'Discount' | $T8 %]</label>
</td>
+ <td>
+ <input name="l_payment" id="l_payment" type="checkbox" class="checkbox" value="Y">
+ <label for="l_payment">[% 'Payment Terms' | $T8 %]</label>
+ </td>
[% IF IS_CUSTOMER %]
<td>
<input name="l_salesman" id="l_salesman" type="checkbox" class="checkbox" value="Y">
--- /dev/null
+<body>
+[% PROCESS 'common/flash.html' %]
+</body>
+</html>
--- /dev/null
+[%- USE HTML %][%- USE LxERP -%]
+<body>
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[% IF !SELF.internal_extensions.size %]
+ <p>[% LxERP.t8("No internal phone extensions have been configured yet.") %]</p>
+
+[% ELSE %]
+ <table>
+ <tr class="listheading">
+ <th>[% LxERP.t8("Name") %]</th>
+ <th>[% LxERP.t8("Phone extension") %]</th>
+ </tr>
+
+ [%- FOREACH extension = SELF.internal_extensions %]
+ <tr class="listrow">
+ <td>[% HTML.escape(extension.name) %]</td>
+ <td><a href="[% HTML.escape(extension.call_link) %]" class="cti_call_action">[% HTML.escape(extension.phone_extension) %]</a></td>
+ </tr>
+ [%- END %]
+ </table>
+[% END %]
+</body>
+</html>
[%- USE LxERP %]
[%- USE L %]
+[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %]
+
[% cv_cvars = SELF.cv.cvars_by_config %]
<form method="post" action="controller.pl">
<th align="right">[% 'IBAN' | $T8 %]</th>
<td>
- [% L.input_tag('cv.iban', SELF.cv.iban, size = 34, maxlength = 100) %]
+ [% L.input_tag('cv.iban', SELF.cv.iban, size = 34) %]
</td>
<th align="right">[% 'BIC' | $T8 %]</th>
<td>
- [% L.input_tag('cv.bic', SELF.cv.bic, size = 20, maxlength = 100) %]
+ [% L.input_tag('cv.bic', SELF.cv.bic, size = 20) %]
</td>
<th align="right">[% 'Bank' | $T8 %]</th>
<th align="right">[% 'Account Number' | $T8 %]</th>
<td>
- [% L.input_tag('cv.account_number', SELF.cv.account_number, size = 20, maxlength = 100) %]
+ [% L.input_tag('cv.account_number', SELF.cv.account_number, size = 20) %]
</td>
<th align="right">[% 'Bank Code Number' | $T8 %]</th>
<td>
- [% L.input_tag('cv.bank_code', SELF.cv.bank_code, size = 20, maxlength = 100) %]
+ [% L.input_tag('cv.bank_code', SELF.cv.bank_code, size = 20) %]
</td>
</tr>
</table>
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
+[%- USE JavaScript -%]
<div id="billing">
<table width="100%">
<th align="right" nowrap>[% 'Department' | $T8 %]</th>
<td>
- [% L.input_tag('cv.department_1', SELF.cv.department_1, size = 16, maxlength = 75) %]
- [% L.input_tag('cv.department_2', SELF.cv.department_2, size = 16, maxlength = 75) %]
+ [% L.input_tag('cv.department_1', SELF.cv.department_1, size = 16) %]
+ [% L.input_tag('cv.department_2', SELF.cv.department_2, size = 16) %]
</td>
</tr>
<th align="right" nowrap>[% 'Street' | $T8 %]</th>
<td>
- [% L.input_tag('cv.street', SELF.cv.street, size = 35, maxlength = 75) %]
+ [% L.input_tag('cv.street', SELF.cv.street, size = 35) %]
<span id="billing_map"></span>
<script type="text/javascript">
- billingMapWidget = new kivi.CustomerVendor.MapWidget('cv_');
+ billingMapWidget = new kivi.CustomerVendor.MapWidget('cv_', '[% JavaScript.escape(SELF.home_address_for_google_maps) %]');
$(function() {
billingMapWidget.render($('#billing_map'));
});
<th align="right" nowrap>[% 'Zipcode' | $T8 %]/[% 'City' | $T8 %]</th>
<td>
- [% L.input_tag('cv.zipcode', SELF.cv.zipcode, size = 5 maxlength = 10) %]
- [% L.input_tag('cv.city', SELF.cv.city, size = 30 maxlength = 75) %]
+ [% L.input_tag('cv.zipcode', SELF.cv.zipcode, size = 5) %]
+ [% L.input_tag('cv.city', SELF.cv.city, size = 30) %]
</td>
</tr>
<th align="right" nowrap>[% 'Country' | $T8 %]</th>
<td>
- [% L.input_tag('cv.country', SELF.cv.country, size = 30 maxlength = 75) %]
+ [% L.input_tag('cv.country', SELF.cv.country, size = 30) %]
</td>
</tr>
<th align="right" nowrap>[% 'Contact' | $T8 %]</th>
<td>
- [% L.input_tag('cv.contact', SELF.cv.contact, size = 28 maxlength = 75) %]
+ [% L.input_tag('cv.contact', SELF.cv.contact, size = 28) %]
</td>
</tr>
<th align="right" nowrap>[% 'Fax' | $T8 %]</th>
<td>
- [% L.input_tag('cv.fax', SELF.cv.fax, size = 30 maxlength = 30) %]
+ [% L.input_tag('cv.fax', SELF.cv.fax, size = 30) %]
</td>
</tr>
<th align="right">[% 'sales tax identification number' | $T8 %]</th>
<td>
- [% L.input_tag('cv.ustid', SELF.cv.ustid, maxlength = 14 size = 20 ) %]
+ [% L.input_tag('cv.ustid', SELF.cv.ustid, size = 20 ) %]
</td>
empty_title = LxERP.t8('New contact'),
value_key = 'cp_id',
title_key = 'full_name',
- onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){contactsMapWidget.testInputs();}});",
+ onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});",
)
%]
</td>
<th align="left" nowrap>[% 'Title' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40 maxlength = 75) %]
+ [% L.input_tag('contact.cp_title', SELF.contact.cp_title, size = 40) %]
[% L.select_tag('contact_cp_title_select', SELF.all_titles, with_empty = 1, onchange = '$("#contact_cp_title").val(this.value);') %]
</td>
</tr>
<th align="left" nowrap>[% 'Function/position' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_position', SELF.contact.cp_position, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_position', SELF.contact.cp_position, size = 40) %]
</td>
</tr>
<th align="left" nowrap>[% 'Given Name' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_givenname', SELF.contact.cp_givenname, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_givenname', SELF.contact.cp_givenname, size = 40) %]
</td>
</tr>
<th align="left" nowrap>[% 'Name' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_name', SELF.contact.cp_name, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_name', SELF.contact.cp_name, size = 40) %]
</td>
</tr>
<th align="left" nowrap>[% 'Phone1' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_phone1', SELF.contact.cp_phone1, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_phone1', SELF.contact.cp_phone1, size = 40) %]
</td>
</tr>
<th align="left" nowrap>[% 'Phone2' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_phone2', SELF.contact.cp_phone2, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_phone2', SELF.contact.cp_phone2, size = 40) %]
</td>
</tr>
<th align="left" nowrap>[% 'Street' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_street', SELF.contact.cp_street, size = 40, maxlength = 75) %]
+ [% L.input_tag('contact.cp_street', SELF.contact.cp_street, size = 40) %]
<span id="contact_map"></span>
<script type="text/javascript">
var contactsMapWidget = new kivi.CustomerVendor.MapWidget('contact_cp_');
<th align="left" nowrap>[% 'Zip, City' | $T8 %]</th>
<td>
- [% L.input_tag('contact.cp_zipcode', SELF.contact.cp_zipcode, size = 5, maxlength = 10) %]
- [% L.input_tag('contact.cp_city', SELF.contact.cp_city, size = 25, maxlength = 75) %]
+ [% L.input_tag('contact.cp_zipcode', SELF.contact.cp_zipcode, size = 5) %]
+ [% L.input_tag('contact.cp_city', SELF.contact.cp_city, size = 25) %]
</td>
</tr>
title_key = 'displayable_id',
with_empty = 1,
empty_title = LxERP.t8('New shipto'),
- onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){shiptoMapWidget.testInputs();}});",
+ onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});",
)
%]
</td>
<th align="right" nowrap>[% 'Name' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptoname', SELF.shipto.shiptoname, size = 35, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptoname', SELF.shipto.shiptoname, size = 35) %]
</td>
</tr>
<th align="right" nowrap>[% 'Abteilung' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptodepartment_1', SELF.shipto.shiptodepartment_1, size = 16, maxlength = 75) %]
- [% L.input_tag('shipto.shiptodepartment_2', SELF.shipto.shiptodepartment_2, size = 16, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptodepartment_1', SELF.shipto.shiptodepartment_1, size = 16) %]
+ [% L.input_tag('shipto.shiptodepartment_2', SELF.shipto.shiptodepartment_2, size = 16) %]
</td>
</tr>
<th align="right" nowrap>[% 'Street' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptostreet', SELF.shipto.shiptostreet, size = 35, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptostreet', SELF.shipto.shiptostreet, size = 35) %]
<span id="shipto_map"></span>
<script type="text/javascript">
<th align="right" nowrap>[% 'Zipcode' | $T8 %]/[% 'City' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptozipcode', SELF.shipto.shiptozipcode, size = 5, maxlength = 75) %]
- [% L.input_tag('shipto.shiptocity', SELF.shipto.shiptocity, size = 30, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptozipcode', SELF.shipto.shiptozipcode, size = 5) %]
+ [% L.input_tag('shipto.shiptocity', SELF.shipto.shiptocity, size = 30) %]
</td>
</tr>
<th align="right" nowrap>[% 'Country' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptocountry', SELF.shipto.shiptocountry, size = 35, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptocountry', SELF.shipto.shiptocountry, size = 35) %]
</td>
</tr>
<th align="right" nowrap>[% 'Contact' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptocontact', SELF.shipto.shiptocontact, size = 30, maxlength = 75) %]
+ [% L.input_tag('shipto.shiptocontact', SELF.shipto.shiptocontact, size = 30) %]
</td>
</tr>
<th align="right" nowrap>[% 'Phone' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptophone', SELF.shipto.shiptophone, size = 30, maxlength = 30) %]
+ [% L.input_tag('shipto.shiptophone', SELF.shipto.shiptophone, size = 30) %]
</td>
</tr>
<th align="right" nowrap>[% 'Fax' | $T8 %]</th>
<td>
- [% L.input_tag('shipto.shiptofax', SELF.shipto.shiptofax, size = 30, maxlength = 30) %]
+ [% L.input_tag('shipto.shiptofax', SELF.shipto.shiptofax, size = 30) %]
</td>
</tr>
<input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
[%- END %]
[%- END %]
- <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]">
- <input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]">
+ <input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+ <input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- UNLESS delivered %]
- <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]">
+ <input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- IF vc == 'customer' %]
- <input class="submit" type="submit" name="action_transfer_out" onclick="return check_transfer_qty()" value="[% 'Transfer out' | $T8 %]">
+ <input class="submit" type="submit" name="action_transfer_out" value="[% 'Transfer out' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% IF transfer_default %]
- <input class="submit" type="submit" name="action_transfer_out_default" value="[% 'Transfer out via default' | $T8 %]">
+ <input class="submit" type="submit" name="action_transfer_out_default" value="[% 'Transfer out via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- END %]
[%- ELSE %]
- <input class="submit" type="submit" name="action_transfer_in" onclick="return check_transfer_qty()" value="[% 'Transfer in' | $T8 %]">
+ <input class="submit" type="submit" name="action_transfer_in" value="[% 'Transfer in' | $T8 %]" data-check-transfer-qty="1" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% IF transfer_default %]
- <input class="submit" type="submit" name="action_transfer_in_default" value="[% 'Transfer in via default' | $T8 %]">
+ <input class="submit" type="submit" name="action_transfer_in_default" value="[% 'Transfer in via default' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- END %]
[%- END %]
[%- END %]
[%- IF id %]
<input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
[%- UNLESS closed %]
- <input class="submit" type="submit" name="action_mark_closed" value="[% 'Mark closed' | $T8 %]">
+ <input class="submit" type="submit" name="action_mark_closed" value="[% 'Mark closed' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- END %]
<input type="button" class="submit" onclick="set_history_window([% id %]);" name="history" id="history" value="[% 'history' | $T8 %]">
[%- END %]
[%- IF id %]
<p>
[% 'Workflow Delivery Order' | $T8 %]<br>
- <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]">
+ <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% UNLESS delivered || (vc == 'customer' && !INSTANCE_CONF.get_sales_delivery_order_show_delete) || (vc == 'vendor' && !INSTANCE_CONF.get_purchase_delivery_order_show_delete) %]
[% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
[% END %]
</form>
<script type='text/javascript'>
- function check_transfer_qty() {
- var all_match = true;
- var rowcount = $('input[name=rowcount]').val();
- for (var i = 1; i < rowcount; i++) {
- if ($('#stock_in_out_qty_matches_' + i).val() != 1) {
- all_match = false;
- }
- }
-
- if (all_match) {
- return true;
- } else {
- return confirm("[% 'There are still transfers not matching the qty of the delivery order. Stock operations can not be changed later. Do you really want to proceed?' | $T8 %]");
- }
- }
+ $(kivi.SalesPurchase.init_on_submit_checks);
</script>
<script type="text/javascript" src="js/calculate_qty.js"></script>
<script type="text/javascript" src="js/stock_in_out.js"></script>
<script type="text/javascript" src="js/follow_up.js"></script>
+ <script type="text/javascript" src="js/kivi.SalesPurchase.js"></script>
<style type="text/css">
.fixed_width {
<form method="post" name="do" action="do.pl">
- <div class="tabwidget">
+ <div id="do_tabs" class="tabwidget">
<ul>
<li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
[%- IF INSTANCE_CONF.get_webdav %]
[%- END %]
[%- ELSE %]
- [% L.select_tag('shipto_id', ALL_SHIPTO, default = shipto_id, value_key = 'shipto_id', title_key = 'displayable_id', with_empty = 1, class = 'fixed_width') %]
+ [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+ L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
[%- END %]
</td>
</tr>
<tr>
<th align="right">[% 'Transaction description' | $T8 %]</th>
- <td colspan="3"><input name="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"[% RO %]></td>
+ <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"[% RO %]></td>
</tr>
</table>
</td>
<td>
- <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y">
+ <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y"[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]>
<label for="l_transaction_description">[% 'Transaction description' | $T8 %]</label>
</td>
</tr>
[%- USE T8 %]
-[%- USE HTML %][%- USE L -%]
+[%- USE HTML %][%- USE LxERP -%][%- USE L -%]
<form name="Form" method="post" action="[% script %]">
<table width="100%">
<th align="left" nowrap>[% 'Message' | $T8 %]</th>
</tr>
<tr>
- <td><textarea name="message" rows="15" cols="60" wrap="soft">[% HTML.escape(message) %]</textarea></td>
+ <td><textarea name="message" id="message" rows="15" cols="60" wrap="soft">[% HTML.escape(message) %]</textarea></td>
</tr>
</table>
<input type="hidden" name="nextsub" value="send_email">
<br>
-<input name="action" class="submit" type="submit" value="[% 'Continue' | $T8 %]">
+[% L.submit_tag('action', LxERP.t8('Continue'), onclick="return check_prerequisites();") %]
</form>
+
+<script type="text/javascript">
+<!--
+function check_prerequisites() {
+ if (!$('#email,#subject,#message').filter(function(idx, elt) { return $(elt).val() === ""; }).size())
+ return true;
+
+ alert(kivi.t8('The recipient, subject or body is missing.'));
+ return false;
+}
+-->
+</script>
<input name="original_partnumber" type="hidden" value="[% HTML.escape(original_partnumber) %]">
<input name="currow" type="hidden" value="[% HTML.escape(currow) %]">
- <div class="tabwidget">
+ <div id="ic_tabs" class="tabwidget">
<ul>
<li><a href="#master_data">[% 'Basic Data' | $T8 %]</a></li>
[% IF LANGUAGES.size %]
-[% USE HTML %][% USE L %][% USE LxERP %]
+[% USE HTML %][% USE L %][% USE LxERP %][%- USE JavaScript -%]
+
+<script type="text/javascript">
+ var addresses = [
+ { shiptoname: "[% JavaScript.escape(vc_obj.name) %]",
+ shiptodepartment_1: "[% JavaScript.escape(vc_obj.department_1) %]",
+ shiptodepartment_2: "[% JavaScript.escape(vc_obj.department_2) %]",
+ shiptostreet: "[% JavaScript.escape(vc_obj.street) %]",
+ shiptozipcode: "[% JavaScript.escape(vc_obj.zipcode) %]",
+ shiptocity: "[% JavaScript.escape(vc_obj.city) %]",
+ shiptocountry: "[% JavaScript.escape(vc_obj.country) %]",
+ shiptocontact: "[% JavaScript.escape(vc_obj.contact) %]",
+ shiptocp_gender: "[% JavaScript.escape(vc_obj.cp_gender) %]",
+ shiptophone: "[% JavaScript.escape(vc_obj.phone) %]",
+ shiptofax: "[% JavaScript.escape(vc_obj.fax) %]",
+ shiptoemail: "[% JavaScript.escape(vc_obj.email) %]"
+ }
+
+ [% FOREACH shipto = vc_obj.shipto %]
+ ,
+ { shiptoname: "[% JavaScript.escape(shipto.shiptoname) %]",
+ shiptodepartment_1: "[% JavaScript.escape(shipto.shiptodepartment_1) %]",
+ shiptodepartment_2: "[% JavaScript.escape(shipto.shiptodepartment_2) %]",
+ shiptostreet: "[% JavaScript.escape(shipto.shiptostreet) %]",
+ shiptozipcode: "[% JavaScript.escape(shipto.shiptozipcode) %]",
+ shiptocity: "[% JavaScript.escape(shipto.shiptocity) %]",
+ shiptocountry: "[% JavaScript.escape(shipto.shiptocountry) %]",
+ shiptocontact: "[% JavaScript.escape(shipto.shiptocontact) %]",
+ shiptocp_gender: "[% JavaScript.escape(shipto.shiptocp_gender) %]",
+ shiptophone: "[% JavaScript.escape(shipto.shiptophone) %]",
+ shiptofax: "[% JavaScript.escape(shipto.shiptofax) %]",
+ shiptoemail: "[% JavaScript.escape(shipto.shiptoemail) %]"
+ }
+ [% END %]
+ ];
+
+ function copy_address() {
+ var shipto = addresses[ $('#shipto_to_copy').val() ];
+ for (key in shipto)
+ $('#' + key).val(shipto[key]);
+ }
+
+ function clear_fields() {
+ var shipto = addresses[0];
+ for (key in shipto)
+ $('#' + key).val('');
+ $('#shiptocp_gender').val('m');
+ }
+
+ function clear_shipto_id_before_submit() {
+ var shipto = addresses[0];
+ for (key in shipto)
+ if ((key != 'shiptocp_gender') && ($('#' + key).val() != '')) {
+ $('#shipto_id').val('');
+ break;
+ }
+
+ $('form').submit();
+ }
+</script>
+
+[% select_options = [ [ 0, LxERP.t8("Billing Address") ] ] ;
+ FOREACH shipto = vc_obj.shipto ;
+ city = shipto.shiptozipcode _ ' ' _ shipto.shiptocity ;
+ title = [ shipto.shiptoname, shipto.shiptostreet, city ] ;
+ CALL select_options.import([ [ loop.count, title.grep('\S').join("; ") ] ]) ;
+ END ;
+ '' %]
<form method="post" action="[% HTML.escape(script) %]">
+ [% L.hidden_tag("shipto_id", shipto_id) %]
+
+ <p>
+ [% LxERP.t8("Copy address from master data") %]:
+ [% L.select_tag("", select_options, id="shipto_to_copy", style="width: 400px") %]
+ [% L.button_tag("copy_address()", LxERP.t8("Copy")) %]
+ [% L.button_tag("clear_fields()", LxERP.t8("Clear fields")) %]
+ </p>
<table>
<tr class="listheading">
[% L.hidden_tag("nextsub", nextsub) %]
[% L.hidden_tag("previousform", previousform) %]
- [% L.submit_tag("__dummy", LxERP.t8("Continue")) %]
+ [% L.button_tag("clear_shipto_id_before_submit()", LxERP.t8("Continue")) %]
</form>
[%- INCLUDE 'common/flash.html' %]
[%- INCLUDE 'generic/set_longdescription.html' %]
-<div class="tabwidget">
+<div id="ir_tabs" class="tabwidget">
<ul>
<li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
[%- IF INSTANCE_CONF.get_webdav %]
<input class="submit" type="submit" accesskey="u" name="action" id="update_button" value="[% 'Update' | $T8 %]">
<input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]">
+ <input class="submit" type="submit" name="action" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+ <input class="submit" type="submit" name="action" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% IF show_storno %]
- <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]">
+ <input class="submit" type="submit" name="action" value="[% 'Storno' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% END %]
<input class="submit" type="submit" name="action" value="[% 'Post Payment' | $T8 %]">
<input class="submit" type="submit" name="action" value="[% 'Use As New' | $T8 %]">
[% IF id && !is_type_credit_note %]
- <input class="submit" type="submit" name="action" value="[% 'Credit Note' | $T8 %]">
+ <input class="submit" type="submit" name="action" value="[% 'Credit Note' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% END %]
[% IF show_delete && !storno %]
<input class="submit" type="submit" name="action" value="[% 'Delete' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
+ <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[% END %]
<input class="submit" type="submit" name="action" value="[% 'Order' | $T8 %]">
<input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
[% UNLESS locked %]
<input class="submit" type="submit" name="action" id="update_button" value="[% 'Update' | $T8 %]">
<input class="submit" type="submit" name="action" value="[% 'Ship to' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Preview' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Post and E-mail' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Print and Post' | $T8 %]">
- <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]">
+ <input class="submit" type="submit" name="action" value="[% 'Preview' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+ <input class="submit" type="submit" name="action" value="[% 'Post and E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+ <input class="submit" type="submit" name="action" value="[% 'Print and Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+ <input class="submit" type="submit" name="action" value="[% 'Post' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
<input class="submit" type="submit" name="action" value="[% 'Save Draft' | $T8 %]">
[%- END %]
[% END # id %]
<input type="hidden" name="customer_discount" value="[% customer_discount %]">
<input type="hidden" name="gldate" value="[% gldate %]">
</form>
+<script type='text/javascript'>
+ $(kivi.SalesPurchase.init_on_submit_checks);
+</script>
[%- PROCESS 'common/flash.html' %]
[%- INCLUDE 'generic/set_longdescription.html' %]
-<div class="tabwidget">
+<div id="is_tabs" class="tabwidget">
<ul>
<li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
[%- IF INSTANCE_CONF.get_webdav %]
<tr>
<th align="right">[% 'Shipping Address' | $T8 %]</th>
<td>
- [% L.select_tag('shipto_id', ALL_SHIPTO, default = shipto_id, value_key = 'shipto_id', title_key = 'displayable_id', with_empty = 1, style='width: 250px', onChange = "document.getElementById('update_button').click();") %]
+ [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+ L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
</td>
</tr>
[%- END %]
</tr>
<tr>
<th align="right">[% 'Transaction description' | $T8 %]</th>
- <td colspan="3"><input size='35' name="transaction_description" value="[% HTML.escape(transaction_description) %]"></td>
+ <td colspan="3"><input size='35' name="transaction_description" id="transaction_description" value="[% HTML.escape(transaction_description) %]"></td>
</tr>
</table>
</td>
--- /dev/null
+[%- USE LxERP -%][%- USE L -%]
+
+<form method="post" action="controller.pl">
+ [% L.hidden_tag('action', 'LiquidityProjection/show') %]
+
+ <table border="0">
+ <tr>
+ <th align="right">[% LxERP.t8("Number of months") %]</th>
+ <td>[% L.input_tag("params.months", FORM.params.months, class="initial_focus") %]</td>
+ </tr>
+
+ <tr>
+ <th align="right" valign="top">[% LxERP.t8("Break down by") %]</th>
+ <td valign="top">
+ [% L.checkbox_tag("params.type", value=1, checked=FORM.params.type, label=LxERP.t8("Basis of calculation")) %]
+ <br>
+ [% L.checkbox_tag("params.salesman", value=1, checked=FORM.params.salesman, label=LxERP.t8("Salesman")) %]
+ <br>
+ [% L.checkbox_tag("params.buchungsgruppe", value=1, checked=FORM.params.buchungsgruppe, label=LxERP.t8("Buchungsgruppe")) %]
+ </td>
+ </tr>
+ </table>
+
+ <p>
+ [% L.submit_tag("dummy", LxERP.t8("Show")) %]
+ </p>
+</form>
--- /dev/null
+[%- USE HTML -%][%- USE LxERP -%]
+[%- SET name_col = FORM.params.salesman || FORM.params.buchungsgruppe || FORM.params.type %]
+
+<table border="0">
+ <tr>
+ <th class="listheading">[% LxERP.t8("Type") %]</th>
+ [%- IF name_col %]
+ <th class="listheading">[% LxERP.t8("Name") %]</th>
+ [%- END %]
+ [%- FOREACH month = SELF.liquidity.sorted.month %]
+ <th class="listheading" align="right">[%- IF month == 'old' %][% LxERP.t8("old") %][% ELSIF month == 'future' %][% LxERP.t8("prospective") %][% ELSE %][%- HTML.escape(month) %][% END %]</th>
+ [%- END %]
+ </tr>
+
+ [% IF FORM.params.type %]
+ [% FOREACH type = SELF.liquidity.sorted.type %]
+ <tr class="listrow">
+ <td>[% IF loop.first %][% LxERP.t8("Basis of calculation") %][% END %]</td>
+ <td>
+ [% IF type == 'order' %][% LxERP.t8("Sales Orders") %]
+ [% ELSIF type == 'partial' %][% LxERP.t8("Partial invoices") %]
+ [% ELSE %][% LxERP.t8("Periodic Invoices") %]
+ [% END %]
+ </td>
+
+ [%- FOREACH month = SELF.liquidity.sorted.month %]
+ <td align="right">[% LxERP.format_amount(SELF.liquidity.$type.$month, 2) %]</td>
+ [%- END %]
+ </tr>
+ [%- END %]
+ [%- END %]
+
+ [%- IF FORM.params.salesman %]
+ [%- FOREACH salesman = SELF.liquidity.sorted.salesman %]
+ <tr class="listrow">
+ <td>[% IF loop.first %][% LxERP.t8("Salesman") %][% END %]</td>
+ <td>[%- HTML.escape(salesman) %]</td>
+
+ [%- FOREACH month = SELF.liquidity.sorted.month %]
+ <td align="right">[% LxERP.format_amount(SELF.liquidity.salesman.$salesman.$month, 2) %]</td>
+ [%- END %]
+ </tr>
+ [%- END %]
+ [%- END %]
+
+ [%- IF FORM.params.buchungsgruppe %]
+ [%- FOREACH buchungsgruppe = SELF.liquidity.sorted.buchungsgruppe %]
+ <tr class="listrow">
+ <td>[% IF loop.first %][% LxERP.t8("Buchungsgruppe") %][% END %]</td>
+ <td>[%- HTML.escape(buchungsgruppe) %]</td>
+
+ [%- FOREACH month = SELF.liquidity.sorted.month %]
+ <td align="right">[% LxERP.format_amount(SELF.liquidity.buchungsgruppe.$buchungsgruppe.$month, 2) %]</td>
+ [%- END %]
+ </tr>
+ [%- END %]
+ [%- END %]
+
+ <tr class="listrow listtotal">
+ <td>[% LxERP.t8("Total") %]</td>
+ [% IF name_col %]<td></td>[% END %]
+ [%- FOREACH month = SELF.liquidity.sorted.month %]
+ <td align="right">
+ [% IF SELF.liquidity.total.$month > 0 %]
+ <a href="[% HTML.escape(SELF.link_to_old_orders(reqdate=month, months=params.months)) %]">
+ [% END %]
+ [% LxERP.format_amount(SELF.liquidity.total.$month, 2) %]
+ [% IF SELF.liquidity.total.$month > 0 %]
+ </a>
+ [% END %]
+ </td>
+ [%- END %]
+ </tr>
+</table>
--- /dev/null
+[% USE HTML %][% USE LxERP %][%- USE L -%]
+<body>
+
+ <h1>[% HTML.escape(title) %]</h1>
+
+ [% PROCESS 'liquidity_projection/_filter.html' %]
+
+ [%- IF SELF.liquidity %]
+ <hr>
+
+ [% PROCESS 'liquidity_projection/_result.html' %]
+ [% END %]
+
+</body>
+</html>
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
+[%- IF is_req_quo || is_pur_ord %]
+ [%- SET allow_invoice=1 %]
+[%- ELSIF is_sales_quo && INSTANCE_CONF.get_allow_sales_invoice_from_sales_quotation %]
+ [%- SET allow_invoice=1 %]
+[%- ELSIF is_sales_ord && INSTANCE_CONF.get_allow_sales_invoice_from_sales_order %]
+ [%- SET allow_invoice=1 %]
+[%- ELSE %]
+ [%- SET allow_invoice=0 %]
+[%- END %]
<tr>
<td>
<table width="100%">
[% label_edit %]<br>
<input class="submit" type="submit" name="action_update" id="update_button" value="[% 'Update' | $T8 %]">
<input class="submit" type="submit" name="action_ship_to" value="[% 'Ship to' | $T8 %]">
-<input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]">
-<input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]">
-<input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]"[% IF warn_save_active_periodic_invoice %] onclick="return warn_save_active_periodic_invoice();"[% END %]>
-<input class="submit" type="submit" name="action_save_and_close" value="[% 'Save and Close' | $T8 %]"[% IF warn_save_active_periodic_invoice %] onclick="return warn_save_active_periodic_invoice();"[% END %]>
+<input class="submit" type="submit" name="action_print" value="[% 'Print' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_e_mail" value="[% 'E-mail' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_save" value="[% 'Save' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
+<input class="submit" type="submit" name="action_save_and_close" value="[% 'Save and Close' | $T8 %]"[% IF warn_save_active_periodic_invoice %] data-warn-save-active-periodic-invoice="1"[% END %] data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- IF id %]
<input type="button" class="submit" onclick="follow_up_window()" value="[% 'Follow-Up' | $T8 %]">
<input type="button" class="submit" onclick="set_history_window([% HTML.escape(id) %])" name="history" id="history" value="[% 'history' | $T8 %]">
<br>[% label_workflow %]<br>
- <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]">
+ <input class="submit" type="submit" name="action_save_as_new" value="[% 'Save as new' | $T8 %]" data-require-transaction-description="[% INSTANCE_CONF.get_require_transaction_description_ps %]">
[%- UNLESS (is_sales_ord && !INSTANCE_CONF.get_sales_order_show_delete) || (is_pur_ord && !INSTANCE_CONF.get_purchase_order_show_delete) %]
[% L.submit_tag('action_delete', LxERP.t8('Delete'), confirm=LxERP.t8('Are you sure?')) %]
<input class="submit" type="submit" name="action_delivery_order" value="[% 'Delivery Order' | $T8 %]">
[%- END %]
+ [%- IF allow_invoice %]
<input class="submit" type="submit" name="action_invoice" value="[% 'Invoice' | $T8 %]">
+ [%- END %]
[%- IF is_sales_ord || is_pur_ord %]
<br>[% heading %] als neue Vorlage verwenden für<br>
[% END %]
</form>
+<script type='text/javascript'>
+ $(kivi.SalesPurchase.init_on_submit_checks);
+</script>
[%- INCLUDE 'common/flash.html' %]
[%- INCLUDE 'generic/set_longdescription.html' %]
- <div class="tabwidget">
+ <div id="oe_tabs" class="tabwidget">
<ul>
<li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
[%- IF INSTANCE_CONF.get_webdav %]
<tr>
<th align="right">[% 'Shipping Address' | $T8 %]</th>
<td>
- [% L.select_tag('shipto_id', ALL_SHIPTO, default=shipto_id, value_key='shipto_id', title_key='displayable_id', with_empty=1, style='width: 250px', onChange="document.getElementById('update_button').click();") %]
+ [% shiptos = [ [ "", LxERP.t8("No/individual shipping address") ] ] ;
+ L.select_tag('shipto_id', shiptos.import(ALL_SHIPTO), default=shipto_id, value_key='shipto_id', title_key='displayable_id', style='width: 250px') %]
</td>
</tr>
[%- END %]
</tr>
<tr>
<th align="right">[% 'Transaction description' | $T8 %]</th>
- <td colspan="3"><input name="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"></td>
+ <td colspan="3"><input name="transaction_description" id="transaction_description" size="35" value="[% HTML.escape(transaction_description) %]"></td>
</tr>
[%- IF show_delivery_customer %]
<tr>
[%- L.select_tag('globalproject_id', ALL_PROJECTS, title_key='projectnumber', default=globalproject_id, with_empty='1', onChange="document.getElementById('update_button').click();") %]
</td>
</tr>
+[%- IF type == 'sales_quotation' %]
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Order probability' | $T8 %]</th>
+ <td nowrap>
+ [%- L.select_tag('order_probability', ORDER_PROBABILITIES, title='title', default=order_probability) %]%
+ </td>
+ </tr>
+ <tr>
+ <th width="70%" align="right" nowrap>[% 'Expected billing date' | $T8 %]</th>
+ <td nowrap>
+ [%- L.date_tag('expected_billing_date', expected_billing_date 'BL') %]
+ </td>
+ </tr>
+[%- END %]
</table>
</td>
</tr>
[% L.date_tag('reqdateto') %]
</td>
</tr>
+[%- IF type == 'sales_quotation' %]
+ <tr>
+ <th align="right">[% 'Expected billing date' | $T8 %] [% 'From' | $T8 %]</th>
+ <td>
+ [% L.date_tag('expected_billing_date_from', '' 'BL') %]
+ </td>
+ <th align="right">[% 'Expected billing date' | $T8 %] [% 'Bis' | $T8 %]</th>
+ <td>
+ [% L.date_tag('expected_billing_date_to', '' 'BL') %]
+ </td>
+ </tr>
+ <tr>
+ <th align="right">[% 'Order probability' | $T8 %]</th>
+ <td colspan="3">
+ [% L.select_tag('order_probability_op', [[ 'ge', '>=' ], [ 'le', '<=' ]]) %]
+ [% L.select_tag('order_probability_value', ORDER_PROBABILITIES, title='title', with_empty=1) %]
+ </td>
+ </tr>
+[%- END %]
<tr>
<th align="right">[% 'Include in Report' | $T8 %]</th>
<td colspan="5">
<label for="l_globalprojectnumber">[% 'Project Number' | $T8 %]</label>
</td>
<td>
- <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y">
+ <input name="l_transaction_description" id="l_transaction_description" class="checkbox" type="checkbox" value="Y"[% IF INSTANCE_CONF.get_require_transaction_description_ps %] checked[% END %]>
<label for="l_transaction_description">[% 'Transaction description' | $T8 %]</label>
</td>
</tr>
<label for="l_salesman">[% 'Salesman' | $T8 %]</label>
</td>
</tr>
+[% IF type == 'sales_quotation' %]
+ <tr>
+ <td colspan="2">
+ <input name="l_order_probability_expected_billing_date" id="l_order_probability_expected_billing_date" class="checkbox" type="checkbox" value="Y">
+ <label for="l_order_probability_expected_billing_date">[% 'Order probability & expected billing date' | $T8 %]</label>
+ </td>
+ </tr>
+[%- END %]
<tr>
<td>
<input name="l_remaining_amount" id="l_remaining_amount" class="checkbox" type="checkbox" value="Y">
[%- IF col.align %] align="[% HTML.escape(col.align) %]" style="text-align: [% HTML.escape(col.align) %]"[% END -%]
[%- IF col.colspan && col.colspan > 1 %] colspan="[% HTML.escape(col.colspan) %]"[% END -%]
>
- [%- IF col.link -%]<a class='report-generator-header-link' href="[% HTML.escape(col.link) %]">[%- END -%]
+ [%- IF col.link -%]<a class="[% col.link_class ? col.link_class : 'report-generator-header-link' %]" href="[% HTML.escape(col.link) %]">[%- END -%]
[%- col.text -%]
[%- IF col.show_sort_indicator -%]<img border="0" src="image/[% IF col.sort_indicator_direction %]down[% ELSE %]up[% END %].png">[%- END -%]
[%- IF col.link -%]</a>[%- END -%]
[%- ELSE %]
[%- USE iterator(col.CELL_ROWS) %][%- FOREACH cell_row = iterator %]
[%- IF cell_row.data != '' %]
- [%- IF cell_row.link %]<a href="[% HTML.escape(cell_row.link) %]">[%- END %]
+ [%- IF cell_row.link %]<a href="[% HTML.escape(cell_row.link) %]"[% IF cell_row.link_class %] class="[% cell_row.link_class %]"[% END %]>[%- END %]
[%- cell_row.data %]
[%- IF cell_row.link %]</a>[%- END %]
[%- END %]
</form>
[% END %]
-
</td>
</tr>
[%- END %]
+[%- BLOCK customer %]
+ <tr>
+ <th align=right nowrap>[% 'Customer' | $T8 %]</th>
+ <td colspan=3>[% L.customer_picker('customer_id') %]</td>
+ </tr>
+[%- END %]
[%- BLOCK projectnumber %]
<tr>
<th align=right nowrap>[% 'Project' | $T8 %]</th>
[%- END %]
[%- IF is_trial_balance %]
+[%- PROCESS customer %]
[%- PROCESS projectnumber %]
<input type=hidden name=nextsub value=generate_trial_balance>
</table>