use SL::BackgroundJob::BackgroundJobCleanup;
use SL::BackgroundJob::CleanBackgroundJobHistory;
use SL::BackgroundJob::CreatePeriodicInvoices;
+use SL::BackgroundJob::FailedBackgroundJobsReport;
1;
--- /dev/null
+package SL::BackgroundJob::FailedBackgroundJobsReport;
+
+use strict;
+use utf8;
+
+use parent qw(SL::BackgroundJob::Base);
+
+use SL::DB::BackgroundJobHistory;
+use SL::Locale::String;
+use SL::Mailer;
+
+use Rose::Object::MakeMethods::Generic (
+ scalar => [ qw(data start_time entries) ],
+);
+
+
+sub create_job {
+ # Don't create this job by default
+ 1;
+}
+
+sub check_config {
+ my ($self) = @_;
+
+ die "No »recipients« specified" unless @{ $self->data->{recipients} || [] };
+ die "No »from« specified" unless $self->data->{from};
+ die "No »subject« specified" unless $self->data->{subject};
+
+ return $self;
+}
+
+sub send_email {
+ my ($self) = @_;
+
+ return 1 unless @{ $self->entries };
+
+ my $template = Template->new({
+ INTERPOLATE => 0,
+ EVAL_PERL => 0,
+ ABSOLUTE => 1,
+ CACHE_SIZE => 0,
+ }) || die("Could not create Template instance");
+
+ my $file_name = $self->data->{template} || 'templates/webpages/failed_background_jobs_report/email.txt';
+ my $body;
+ $template->process($file_name, { SELF => $self }, \$body);
+ $body = Encode::decode('utf-8', $body);
+
+ Mailer->new(
+ from => $self->data->{from},
+ to => join(', ', @{ $self->data->{recipients} }),
+ subject => $self->data->{subject},
+ content_type => 'text/plain',
+ charset => 'utf-8',
+ message => $body,
+ )->send;
+
+ return $self;
+}
+
+sub load_failed_entries {
+ my ($self) = @_;
+
+ $self->start_time(DateTime->now_local->subtract(days => 1));
+ $self->entries([ @{ SL::DB::Manager::BackgroundJobHistory->get_all(
+ sort_by => 'run_at ASC',
+ where => [
+ status => SL::DB::BackgroundJobHistory::FAILURE(),
+ run_at => { ge => $self->start_time },
+ ],
+ )}]);
+
+ return $self;
+}
+
+sub run {
+ my ($self, $db_obj) = @_;
+
+ $self->data($db_obj->data_as_hash);
+
+ $self
+ ->check_config
+ ->load_failed_entries
+ ->send_email;
+
+ return 1;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::BackgroundJob::FailedBackgroundJobsReport - A background job
+checking for failed jobs and reporting them via email
+
+=head1 OVERVIEW
+
+This background's job is to watch over other background jobs. It will
+determine when it has run last and look for job history entries that
+have failed between the last run and the current time.
+
+If that search yields results then an email will be sent listing the
+jobs that failed and the error messages they produced. The template
+used for the email's body defaults to the file
+C<templates/webpages/failed_background_jobs_report/email.txt> but can
+be overridden in the configuration.
+
+This background job is not active by default. You have to add and
+configure it manually.
+
+=head1 CONFIGURATION
+
+This background job requires configuration data stored in its data
+member. This is supposed to be a YAML-encoded hash of the following
+options:
+
+=over 4
+
+=item * C<from> – required; the sender's email address used in the
+mail headers
+
+=item * C<recipients> – required; an array of email addresses for the
+recipients
+
+=item * C<subject> – required; the email's subject
+
+=item * C<template> – optional; a file name pointing to the template
+file used for the email's body. This defaults to
+C<templates/webpages/failed_background_jobs_report/email.txt>.
+
+=back
+
+Here's an example of how this data looks like:
+
+ ---
+ from: kivitendo@meine.firma
+ recipients:
+ - johanna.admin@meine.firma
+ subject: Fehlgeschlagene kivitendo-Jobs der letzten 24h
+ template: templates/mycompany/faileed_background_jobs_email.txt
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
sub init_modules {
my ($self, %params) = @_;
- return [
- { module => 'CT', description => t8('Customers and vendors') },
- { module => 'Contacts', description => t8('Contact persons') },
- { module => 'IC', description => t8('Parts, services and assemblies') },
- { module => 'Projects', description => t8('Projects') },
- ];
+ return [ sort { $a->{description}->translated cmp $b->{description}->translated } (
+ { module => 'CT', description => t8('Customers and vendors') },
+ { module => 'Contacts', description => t8('Contact persons') },
+ { module => 'IC', description => t8('Parts, services and assemblies') },
+ { module => 'Projects', description => t8('Projects') },
+ { module => 'RequirementSpecs', description => t8('Requirement Specs') },
+ )];
}
sub create_or_update {
_add_uniq($objects, $_) for @$auto_objects;
}
- my $query = _parse_filter($flattened, $objects, %params);
-
_launder_keys($filter, $params{launder_to}) unless $params{no_launder};
+ my $query = _parse_filter($flattened, $objects, %params);
+
return
($query && @$query ? (query => $query) : ()),
($objects && @$objects ? ( with_objects => [ uniq @$objects ]) : ());
die 'unrecognized filters' if $key =~ /:/;
my @tokens = split /\./, $key;
- my $last_token = pop @tokens;
my $curr_class = $class->object_class;
- for my $token (@tokens) {
+ # find first token which is not a relationship
+ my $i = 0;
+ while ($i < $#tokens) {
eval {
- $curr_class = $curr_class->meta->relationship($token)->class;
- 1;
+ $curr_class = $curr_class->meta->relationship($tokens[$_])->class;
+ ++$i;
} or do {
- require Carp;
- Carp::croak("Could not resolve the relationship '$token' in '$key' while building the filter request");
+ last;
}
}
my $manager = $curr_class->meta->convention_manager->auto_manager_class_name;
- my $obj_path = join '.', @tokens;
- my $obj_prefix = join '.', @tokens, '';
+ my $obj_path = join '.', @tokens[0..$i-1];
+ my $obj_prefix = join '.', @tokens[0..$i-1], '';
+ my $key_token = $tokens[$i];
+ my @additional_tokens = @tokens[$i+1..$#tokens];
if ($manager->can('filter')) {
- ($key, $value, my $obj) = $manager->filter($last_token, $value, $obj_prefix, $obj_path);
+ ($key, $value, my $obj) = $manager->filter($key_token, $value, $obj_prefix, $obj_path, @additional_tokens);
_add_uniq($with_objects, $obj) if $obj;
} else {
_add_uniq($with_objects, $obj_path) if $obj_path;
(
scalar => [ qw(requirement_spec_item visible_item visible_section) ],
'scalar --get_set_init' => [ qw(requirement_spec customers types statuses complexities risks projects project_types project_statuses default_project_type default_project_status copy_source js
- current_text_block_output_position models time_based_units html_template) ],
+ current_text_block_output_position models time_based_units html_template cvar_configs includeable_cvar_configs include_cvars) ],
);
__PACKAGE__->run_before('setup');
$::auth->assert('requirement_spec_edit');
$::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec);
- $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu jquery/jquery.hotkeys requirement_spec ckeditor/ckeditor ckeditor/adapters/jquery autocomplete_part);
+ $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu jquery/jquery.hotkeys requirement_spec ckeditor/ckeditor ckeditor/adapters/jquery autocomplete_part autocomplete_customer);
$self->init_visible_section;
return 1;
sub init_statuses { SL::DB::Manager::RequirementSpecStatus->get_all_sorted }
sub init_time_based_units { SL::DB::Manager::Unit->time_based_units }
sub init_types { SL::DB::Manager::RequirementSpecType->get_all_sorted }
+sub init_cvar_configs { SL::DB::Manager::CustomVariableConfig->get_all_sorted(where => [ module => 'RequirementSpecs' ]) }
sub init_customers {
my ($self) = @_;
$self->current_text_block_output_position($::form->{current_content_type} !~ m/^(?:text-blocks|tb)-(front|back)/ ? -1 : $1 eq 'front' ? 0 : 1);
}
+sub init_includeable_cvar_configs {
+ my ($self) = @_;
+ return [ grep { $_->includeable } @{ $self->cvar_configs } ];
+}
+
+sub init_include_cvars {
+ my ($self) = @_;
+ return $::form->{include_cvars} if $::form->{include_cvars} && (ref($::form->{include_cvars}) eq 'HASH');
+ return { map { ($_->name => ($_->includeable && $_->included_by_default)) } @{ $self->cvar_configs } };
+}
+
#
# helpers
#
my $self = shift;
my $is_new = !$self->requirement_spec->id;
my $params = delete($::form->{requirement_spec}) || { };
+ my $cvars = delete($::form->{cvars}) || { };
+
+ # Forcefully make it clear to Rose which custom_variables exist (or don't), so that the ones added with »add_custom_variables« are visible when calling »custom_variables«.
+ if ($is_new) {
+ $params->{custom_variables} = [];
+ } else {
+ $self->requirement_spec->custom_variables;
+ }
$self->requirement_spec->assign_attributes(%{ $params });
+ foreach my $var (@{ $self->requirement_spec->cvars_by_config }) {
+ my $value = $cvars->{ $var->config->name };
+ $value = $::form->parse_amount(\%::myconfig, $value) if $var->config->type eq 'number';
+
+ $var->value($value);
+ }
+
my $title = $is_new && $self->requirement_spec->is_template ? t8('Create a new requirement spec template')
: $is_new ? t8('Create a new requirement spec')
: $self->requirement_spec->is_template ? t8('Edit requirement spec template')
if ($self->copy_source) {
$self->requirement_spec($self->copy_source->create_copy(%{ $params }));
} else {
- $self->requirement_spec->save;
+ $self->requirement_spec->save(cascade => 1);
}
})) {
$::lxdebug->message(LXDebug::WARN(), "Error: " . $db->error);
);
}
- map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
+ $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) for keys %column_defs;
+
+ if (!$is_template) {
+ my %cvar_column_defs = map {
+ my $cfg = $_;
+ (('cvar_' . $cfg->name) => {
+ sub => sub { my $var = $_[0]->cvar_by_name($cfg->name); $var ? $var->value_as_text : '' },
+ text => $cfg->description,
+ visible => $self->include_cvars->{ $cfg->name } ? 1 : 0,
+ })
+ } @{ $self->includeable_cvar_configs };
+
+ push @columns, map { 'cvar_' . $_->name } @{ $self->includeable_cvar_configs };
+ %column_defs = (%column_defs, %cvar_column_defs);
+ }
$report->set_options(
std_column_visibility => 1,
return (any { $_ eq $self->default_value } @{ $self->processed_options }) ? $self->default_value : $self->processed_options->[0];
}
+sub value_col {
+ my ($self) = @_;
+
+ my $type = $self->type;
+
+ return {
+ bool => 'bool_value',
+ timestamp => 'timestamp_value',
+ date => 'timestamp_value',
+ number => 'number_value',
+ integer => 'number_value',
+ customer => 'number_value',
+ vendor => 'number_value',
+ part => 'number_value',
+ text => 'text_value',
+ textfield => 'text_value',
+ select => 'text_value'
+ }->{$type};
+}
+
1;
make_cvar_by_name($caller_package, %params);
make_cvar_as_hashref($caller_package, %params);
make_cvar_value_parser($caller_package, %params);
+ make_cvar_custom_filter($caller_package, %params);
}
sub save_meta_info {
return $column_name;
}
+sub make_cvar_custom_filter {
+ my ($caller_package, %params) = @_;
+
+ my $manager = $caller_package->meta->convention_manager->auto_manager_class_name;
+
+ return unless $manager->can('filter');
+
+ $manager->add_filter_specs(
+ cvar => sub {
+ my ($key, $value, $prefix, $config_id) = @_;
+ my $config = SL::DB::Manager::CustomVariableConfig->find_by(id => $config_id);
+
+ if (!$config) {
+ die "invalid config_id in $caller_package\::cvar custom filter: $config_id";
+ }
+
+ if ($config->module != $params{module}) {
+ die "invalid config_id in $caller_package\::cvar custom filter: expected module $params{module} - got @{[ $config->module ]}";
+ }
+
+ my @filter;
+ if ($config->type eq 'bool') {
+ @filter = $value ? ($config->value_col => 1) : (or => [ $config->value_col => undef, $config->value_col => 0 ]);
+ } else {
+ @filter = ($config->value_col => $value);
+ }
+
+ my (%query, %bind_vals);
+ ($query{customized}, $bind_vals{customized}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'trans_id',
+ tables => [ 'custom_variables' ],
+ columns => { custom_variables => [ qw(trans_id config_id text_value number_value bool_value timestamp_value sub_module) ] },
+ query => [
+ config_id => $config_id,
+ sub_module => $params{sub_module},
+ @filter,
+ ],
+ query_is_sql => 1,
+ );
+
+ if ($config->type eq 'bool') {
+ if ($value) {
+ @filter = (
+ '!default_value' => undef,
+ '!default_value' => '',
+ default_value => '1',
+ );
+
+ } else {
+ @filter = (
+ or => [
+ default_value => '0',
+ default_value => '',
+ default_value => undef,
+ ],
+ );
+ }
+
+ } else {
+ @filter = (
+ '!default_value' => undef,
+ '!default_value' => '',
+ default_value => $value,
+ );
+ }
+
+
+ my $conversion = $config->type =~ m{^(?:date|timestamp)$} ? $config->type
+ : $config->type =~ m{^(?:customer|vendor|part)$} ? 'integer'
+ : $config->type eq 'number' ? 'numeric'
+ : '';
+
+ ($query{config}, $bind_vals{config}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'id',
+ tables => [ 'custom_variable_configs' ],
+ columns => { custom_variable_configs => [ qw(id default_value) ] },
+ query => [
+ id => $config->id,
+ @filter,
+ ],
+ query_is_sql => 1,
+ );
+
+ $query{config} =~ s{ (?<! NOT\( ) default_value (?! \s*is\s+not\s+null) }{default_value::${conversion}}x if $conversion;
+
+ ($query{not_customized}, $bind_vals{not_customized}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'trans_id',
+ tables => [ 'custom_variables' ],
+ columns => { custom_variables => [ qw(trans_id config_id sub_module) ] },
+ query => [
+ config_id => $config_id,
+ sub_module => $params{sub_module},
+ ],
+ query_is_sql => 1,
+ );
+
+ foreach my $key (keys %query) {
+ # remove rose aliases. query builder sadly is not reentrant, and will reuse the same aliases. :(
+ $query{$key} =~ s{\bt\d+(?:\.)?\b}{}g;
+
+ # manually inline the values. again, rose doen't know how to handly bind params in subqueries :(
+ $query{$key} =~ s{\?}{ $config->dbh->quote(shift @{ $bind_vals{$key} }) }xeg;
+
+ $query{$key} =~ s{\n}{ }g;
+ }
+
+ my $qry_config = "EXISTS (" . $query{config} . ")";
+
+ my @result = (
+ 'or' => [
+ $prefix . 'id' => [ \$query{customized} ],
+ and => [
+ "!${prefix}id" => [ \$query{not_customized} ],
+ \$qry_config,
+ ]
+ ],
+ );
+
+ return @result;
+ }
+ );
+}
+
1;
__END__
=back
+=head1 INSTALLED MANAGER METHODS
+
+=over 4
+
+=item Custom filter for GetModels
+
+If the Manager for the calling C<SL::DB::Object> has included the helper L<SL::DB::Helper::Filtered>, a custom filter for cvars will be added to the specs, with the following syntax:
+
+ filter.cvar.$config_id
+
+=back
+
=head1 AUTHOR
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
my %filter_spec;
sub filter {
- my ($class, $key, $value, $prefix, $path) = @_;
+ my ($class, $key, $value, $prefix, $path, @additional_tokens) = @_;
my $filters = _get_filters($class);
return ($prefix . $key, $value, $path) unless $filters->{$key};
- return $filters->{$key}->($key, $value, $prefix);
+ return $filters->{$key}->($key, $value, $prefix, @additional_tokens);
}
sub _get_filters {
use SL::DB::Helper::Manager;
use base qw(SL::DB::Helper::Manager);
+use SL::DB::Helper::Filtered;
use SL::DB::Helper::Paginated;
use SL::DB::Helper::Sorted;
rmanumber => { type => 'text' },
sales_delivery_order_show_delete => { type => 'boolean', default => 'true' },
sales_order_show_delete => { type => 'boolean', default => 'true' },
+ sales_purchase_order_ship_missing_column => { type => 'boolean', default => 'false' },
sdonumber => { type => 'text' },
sepa_creditor_id => { type => 'text' },
servicenumber => { type => 'text' },
use SL::DB::MetaSetup::RequirementSpec;
use SL::DB::Manager::RequirementSpec;
use SL::DB::Helper::AttrDuration;
+use SL::DB::Helper::CustomVariables (
+ module => 'RequirementSpecs',
+ cvars_alias => 1,
+);
use SL::DB::Helper::LinkedRecords;
use SL::Locale::String;
use SL::Util qw(_hashify);
%attributes);
}
+ # Copy custom variables.
+ foreach my $var (@{ $source->cvars_by_config }) {
+ $self->cvar_by_name($var->config->name)->value($var->value);
+ }
+
my %paste_template_result;
# Clone text blocks and pictures.
$self->$accessor($paste_template_result{parts});
# Save new object -- we need its ID for the items.
- $self->save;
+ $self->save(cascade => 1);
my %id_to_clone;
my $is_quotation = $form->{type} =~ /_quotation$/;
my $is_invoice = $form->{type} =~ /invoice/;
my $is_s_p_order = (first { $_ eq $form->{type} } qw(sales_order purchase_order));
+ my $show_ship_missing = $is_s_p_order && $::instance_conf->get_sales_purchase_order_ship_missing_column;
if ($is_delivery_order) {
if ($form->{type} eq 'sales_delivery_order') {
{ id => 'partnumber', width => 8, value => $locale->text('Number'), display => 1, },
{ id => 'description', width => 30, value => $locale->text('Part Description'), display => 1, },
{ id => 'ship', width => 5, value => $locale->text('Delivered'), display => $is_s_p_order, },
+ { id => 'ship_missing', width => 5, value => $locale->text('Not delivered'), display => $show_ship_missing, },
{ id => 'qty', width => 5, value => $locale->text('Qty'), display => 1, },
{ id => 'price_factor', width => 5, value => $locale->text('Price Factor'), display => !$is_delivery_order, },
{ id => 'unit', width => 5, value => $locale->text('Unit'), display => 1, },
my $deliverydate = $locale->text('Required by');
# special alignings
- my %align = map { $_ => 'right' } qw(qty ship right discount linetotal stock_in_out weight);
+ my %align = map { $_ => 'right' } qw(qty ship right discount linetotal stock_in_out weight ship_missing);
my %nowrap = map { $_ => 1 } qw(description unit);
$form->{marge_total} = 0;
$ship_qty /= ( $all_units->{$form->{"unit_$i"}}->{factor} || 1 );
$column_data{ship} = $form->format_amount(\%myconfig, $form->round_amount($ship_qty, 2) * 1) . ' ' . $form->{"unit_$i"};
+
+ my $ship_missing_qty = $form->{"qty_$i"} - $ship_qty;
+ my $ship_missing_amount = $form->round_amount($ship_missing_qty * $form->{"sellprice_$i"} * (100 - $form->{"discount_$i"}) / 100 / $price_factor, 2);
+
+ $column_data{ship_missing} = $form->format_amount(\%myconfig, $ship_missing_qty) . ' ' . $form->{"unit_$i"} . '; ' . $form->format_amount(\%myconfig, $ship_missing_amount, $decimalplaces);
}
my $sellprice_value = $form->format_amount(\%myconfig, $form->{"sellprice_$i"}, $decimalplaces);
return $obj;
}
-
/* ------------------------------------------------------------ */
input.rs_input_field, select.rs_input_field,
-table.rs_input_field input, table.rs_input_field select {
+table.rs_input_field input[type=text], table.rs_input_field input[type=password], table.rs_input_field select {
width: 300px;
}
'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 Verkaufsaufträge nicht direkt in Verkaufsrechnungen umgewandelt werden.',
'If disabled sales quotations cannot be converted into sales invoices directly.' => 'Falls deaktiviert, so können Verkaufsangebote nicht direkt in Verkaufsrechnungen umgewandelt werden.',
+ 'If enabled a column will be shown in sales and purchase orders that lists both the amount and the value not shipped yet for each item.' => 'Falls eingeschaltet, wird für jede Position in Auftragsbestätigungen und Lieferantenaufträgen eine Spalte mit noch nicht gelieferter Menge und Wert angezeigt.',
'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.',
'Show the weights of articles and the total weight in orders, invoices and delivery notes?' => 'Sollen Warengewichte und Gesamtgewicht in Aufträgen, Rechnungen und Lieferscheinen angezeigt werden?',
'Show weights' => 'Gewichte anzeigen',
'Show your TODO list after logging in' => 'Aufgabenliste nach dem Anmelden anzeigen',
+ 'Show »not delivered qty/value« column in sales and purchase orders' => 'Spalte »Nicht gelieferte Menge/Wert« in Aufträgen anzeigen',
'Signature' => 'Unterschrift',
'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerplätze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
'Single quotes' => 'Einfache Anführungszeichen',
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 task_server/failure_notification_email.txt)
+ 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
+ failed_background_jobs_report/email.txt)
);
my (%referenced_html_files, %locale, %htmllocale, %alllocales, %cached, %submit, %jslocale);
--- /dev/null
+-- @tag: custom_variables_delete_via_trigger_requirement_specs
+-- @description: Benutzerdefinierte Variablen von Pflichtenheften via Trigger löschen
+-- @depends: custom_variables_delete_via_trigger requirement_specs
+CREATE OR REPLACE FUNCTION delete_requirement_spec_custom_variables_trigger() RETURNS trigger AS $$
+ BEGIN
+ DELETE FROM custom_variables WHERE (sub_module = '' OR sub_module IS NULL)
+ AND trans_id = OLD.id
+ AND (SELECT module FROM custom_variable_configs WHERE id = config_id) = 'RequirementSpecs';
+
+ RETURN OLD;
+ END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS delete_requirement_spec_custom_variables ON requirement_specs;
+
+CREATE TRIGGER delete_requirement_spec_custom_variables
+BEFORE DELETE ON requirement_specs
+FOR EACH ROW EXECUTE PROCEDURE delete_requirement_spec_custom_variables_trigger();
--- /dev/null
+-- @tag: defaults_sales_purchase_order_show_ship_missing_column
+-- @description: Mandantenkonfiguration: Optionale Spalte »Nicht gelieferte Menge« in Auftragsbestätigungen und Lieferantenaufträgen
+-- @depends: release_3_1_0
+
+ALTER TABLE defaults ADD COLUMN sales_purchase_order_ship_missing_column BOOLEAN;
+UPDATE defaults SET sales_purchase_order_ship_missing_column = FALSE;
+ALTER TABLE defaults ALTER COLUMN sales_purchase_order_ship_missing_column SET DEFAULT FALSE;
use lib 't';
-use Test::More tests => 31;
+use Test::More tests => 36;
use Test::Deep;
use Data::Dumper;
}
}, 'deep laundering, check for laundered hash', target => 'launder', launder_to => { };
+test {
+ part => {
+ 'sellprice:number' => '2',
+ 'sellprice:number::' => 'le',
+ }
+}, {
+ part => {
+ 'sellprice:number' => '2',
+ 'sellprice:number::' => 'le',
+ }
+}, 'laundering of indirect filters does not alter', target => 'filter', launder_to => { };
+
+test {
+ part => {
+ 'sellprice:number' => '2',
+ 'sellprice:number::' => 'le',
+ }
+}, {
+ part => {
+ 'sellprice_number' => '2',
+ 'sellprice_number__' => 'le',
+ }
+}, 'laundering of indirect filters', target => 'launder', launder_to => { };
+
+test {
+ part => {
+ 'sellprice:number' => '2',
+ 'sellprice:number::' => 'le',
+ }
+}, {
+ part => {
+ 'sellprice:number' => '2',
+ 'sellprice:number::' => 'le',
+ 'sellprice_number' => '2',
+ 'sellprice_number__' => 'le',
+ }
+}, 'laundering of indirect filters - inplace', target => 'filter';
+
### bug: sub objects
test {
]
]
}, ':multi with complex tokenizing';
+
+# test tokenizing for custom filters by monkeypatching a custom filter into Part
+SL::DB::Manager::Part->add_filter_specs(
+ test => sub {
+ my ($key, $value, $prefix, @additional) = @_;
+ return "$prefix$key" => { @additional, $value };
+ }
+);
+
+test {
+ 'part.test.what' => 2,
+}, {
+ query => [
+ 'part.test' => { 'what', 2 },
+ ]
+}, 'additional tokens', class => 'SL::DB::Manager::OrderItem';
+
+test {
+ 'part.test.what:substr::ilike' => 2,
+}, {
+ query => [
+ 'part.test' => { 'what', { ilike => '%2%' } },
+ ]
+}, 'additional tokens + filters + methods', class => 'SL::DB::Manager::OrderItem';
<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 align="right">[% LxERP.t8("Show »not delivered qty/value« column in sales and purchase orders") %]</td>
+ <td>[% L.yes_no_tag("defaults.sales_purchase_order_ship_missing_column", SELF.defaults.sales_purchase_order_ship_missing_column) %]</td>
+ <td>[% LxERP.t8("If enabled a column will be shown in sales and purchase orders that lists both the amount and the value not shipped yet for each item.") %]</td>
+ </tr>
+
<tr><td class="listheading" colspan="4">[% LxERP.t8("E-mail") %]</td></tr>
<tr>
--- /dev/null
+[%- USE HTML -%][%- USE L -%][%- USE LxERP -%][%- USE T8 -%]
+[%- SET id__ = cvar_cfg.id
+ name__ = 'filter.cvar.' _ id__
+ value__ = filter.cvar.$id__ %]
+[%- IF cvar_cfg.type == 'bool' %]
+ [%- L.select_tag(name__, [ '', [ 1, LxERP.t8('Yes') ], [ 0, LxERP.t8('No') ] ], default=value__, class=cvar_class) %]
+
+[%- ELSIF cvar_cfg.type == 'number' %]
+ [% L.select_tag(name__ _ '::', [ [ 'eq', '==' ], [ 'ne', '=/=' ], [ 'gt', '>' ], [ 'ge', '>=' ], [ 'lt', '<' ], [ 'le', '<=' ] ], default=filter.cvar.item(cvar_cfg.id _ '__')) %]
+ [% L.input_tag(name__, value__, class=cvar_class) %]
+
+[%- ELSIF cvar_cfg.type == 'date' %]
+ [% L.select_tag(name__ _ '::', [ [ 'eq', '==' ], [ 'ne', '=/=' ], [ 'gt', '>' ], [ 'ge', '>=' ], [ 'lt', '<' ], [ 'le', '<=' ] ], default=filter.cvar.item(cvar_cfg.id _ '__')) %]
+ [% L.date_tag(name__, value__, class=cvar_class) %]
+
+[% ELSIF cvar_cfg.type == 'select' %]
+ [% options__ = [ '' ];
+ options__ = options__.import(cvar_cfg.processed_options);
+ L.select_tag(name__, options__, default=value__, class=cvar_class) %]
+
+[% ELSIF cvar_cfg.type == 'customer' %]
+ [%- L.customer_vendor_picker(name__, value__, type='customer', class=cvar_class) %]
+
+[% ELSIF cvar_cfg.type == 'vendor' %]
+ [%- L.customer_vendor_picker(name__, value__, type='vendor', class=cvar_class) %]
+
+[% ELSIF cvar_cfg.type == 'part' %]
+ [%- L.part_picker(name__, value__, class=cvar_class) %]
+
+[%- ELSE %]
+ [% SET value_name__ = id__ _ '_substr__ilike'
+ value__ = filter.cvar.$value_name__ %]
+ [%- L.input_tag(name__ _ ':substr::ilike', value__, maxlength=cvar_cfg.maxlength, class=cvar_class) %]
+
+[%- END %]
--- /dev/null
+Hallo,
+
+die folgenden Hintergrundjobs sind seit [% SELF.start_time.to_kivitendo %] [% SELF.start_time.to_kivitendo_time %] ausgeführt worden und schlugen fehl:
+
+[%- FOREACH entry = SELF.entries %]
+Paketname: [% entry.package_name %]
+Ausgeführt um: [% entry.run_at.to_kivitendo %] [% entry.run_at.to_kivitendo_time %]
+Fehler: [% entry.error_col %]
+[% UNLESS loop.last %]============================================================[% END %]
+[%- END %]
+Gruß,
+kivitendo
[%- L.hidden_tag("is_template", is_template) %]
<p>
- <table class="rs_input_field">
+ <table>
<tr>
<th align="right">[% LxERP.t8("Title") %]</th>
- <td>[% L.input_tag('filter.title:substr::ilike', filter.title_substr__ilike) %]</td>
+ <td>[% L.input_tag('filter.title:substr::ilike', filter.title_substr__ilike, class="rs_input_field") %]</td>
</tr>
[%- UNLESS is_template %]
<tr>
<th align="right">[% LxERP.t8("Customer") %]</th>
- <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike) %]</td>
+ <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, class="rs_input_field") %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8("Customer Number") %]</th>
- <td>[% L.input_tag('filter.customer.customernumber:substr::ilike', filter.customer.customernumber_substr__ilike) %]</td>
+ <td>[% L.input_tag('filter.customer.customernumber:substr::ilike', filter.customer.customernumber_substr__ilike, class="rs_input_field") %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8("Requirement Spec Type") %]</th>
- <td>[% L.select_tag('filter.type_id', SELF.types, default=filter.type_id, title_key="description", with_empty=1) %]</td>
+ <td>[% L.select_tag('filter.type_id', SELF.types, default=filter.type_id, title_key="description", with_empty=1, class="rs_input_field") %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8("Requirement Spec Status") %]</th>
- <td>[% L.select_tag('filter.status_id[]', SELF.statuses, default=filter.status_id_, title_key="description", multiple=1) %][%# NOTE: the trailing '_' is NOT a mistake -- look at SL::Controller::Helper::Filtered for the explanation! %]</td>
+ <td>[% L.select_tag('filter.status_id[]', SELF.statuses, default=filter.status_id_, title_key="description", multiple=1, class="rs_input_field") %][%# NOTE: the trailing '_' is NOT a mistake -- look at SL::Controller::Helper::Filtered for the explanation! %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8("Project Number") %]</th>
- <td>[% L.input_tag('filter.project.projectnumber:substr::ilike', filter.project.projectnumber_substr__ilike) %]</td>
+ <td>[% L.input_tag('filter.project.projectnumber:substr::ilike', filter.project.projectnumber_substr__ilike, class="rs_input_field") %]</td>
</tr>
<tr>
<th align="right">[% LxERP.t8("Project Description") %]</th>
- <td>[% L.input_tag('filter.project.description:substr::ilike', filter.project.description_substr__ilike) %]</td>
+ <td>[% L.input_tag('filter.project.description:substr::ilike', filter.project.description_substr__ilike, class="rs_input_field") %]</td>
</tr>
+
+ [% FOREACH cvar_cfg = SELF.cvar_configs %]
+ [%- IF cvar_cfg.searchable %]
+ <tr>
+ <th align="right">[% HTML.escape(cvar_cfg.description) %]</th>
+ <td>[% INCLUDE 'common/render_cvar_filter_input.html' cvar_cfg=cvar_cfg cvar_class="rs_input_field" %]</td>
+ </tr>
+ [% END %]
+ [% END %]
+
+ [% L.hidden_tag("include_cvars.dummy__", 1) %]
+ [% IF SELF.includeable_cvar_configs.size %]
+ <tr>
+ <th align="right">[% LxERP.t8("Include in Report") %]</th>
+ <td>
+ <table>
+ <tr>
+ [% FOREACH cvar_cfg = SELF.includeable_cvar_configs %]
+ <td>
+ [% name__ = cvar_cfg.name;
+ L.checkbox_tag("include_cvars." _ name__, value="1", checked=(SELF.include_cvars.$name__ ? 1 : ''), label=cvar_cfg.description) %]
+ </td>
+ [%- IF !loop.last && ((loop.count % 3) == 0) %]
+ </tr><tr>
+ [% END %]
+ [% END %]
+ </tr>
+ </table>
+ </td>
+ </tr>
+ [% END %]
[%- END %]
</table>
</p>
-[%- USE LxERP -%][%- USE L -%]
+[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
[%- DEFAULT id_prefix = 'basic_settings_form'
submit_as = 'post'
%]
[%- END %]
+ [% cvars = SELF.requirement_spec.cvars_by_config %]
+
+ [% FOREACH var = cvars %]
+ <tr>
+ <td>[% HTML.escape(var.config.description) %]</td>
+
+ <td>
+ [% INCLUDE 'common/render_cvar_input.html'
+ cvar_name_prefix = 'cvars.'
+ %]
+ </td>
+ </tr>
+ [% END %]
+
</table>
[%- IF SELF.copy_source %]
<td>[% HTML.escape(SELF.requirement_spec.hourly_rate_as_number) %]</td>
</tr>
+ [% cvars = SELF.requirement_spec.cvars_by_config %]
+
+ [% FOREACH var = cvars %]
+ <tr class="listrow">
+ <td>[% HTML.escape(var.config.description) %]</td>
+ <td>[% HTML.escape(var.value_as_text) %]</td>
+ </tr>
+ [% END %]
+
[%- END %]
</table>