Revision 9cddaf37
Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt
SL/Controller/RequirementSpec.pm | ||
---|---|---|
5 | 5 |
|
6 | 6 |
use parent qw(SL::Controller::Base); |
7 | 7 |
|
8 |
use File::Spec (); |
|
9 |
|
|
8 | 10 |
use SL::ClientJS; |
11 |
use SL::Common (); |
|
9 | 12 |
use SL::Controller::Helper::GetModels; |
10 | 13 |
use SL::Controller::Helper::Filtered; |
11 | 14 |
use SL::Controller::Helper::Paginated; |
... | ... | |
208 | 211 |
my ($self, %params) = @_; |
209 | 212 |
|
210 | 213 |
my $base_name = $self->requirement_spec->type->template_file_name || 'requirement_spec'; |
214 |
my @pictures = $self->prepare_pictures_for_printing; |
|
211 | 215 |
my %result = SL::Template::LaTeX->parse_and_create_pdf("${base_name}.tex", SELF => $self, rspec => $self->requirement_spec); |
212 | 216 |
|
217 |
unlink @pictures unless ($::lx_office_conf{debug} || {})->{keep_temp_files}; |
|
218 |
|
|
213 | 219 |
$::form->error(t8('Conversion to PDF failed: #1', $result{error})) if $result{error}; |
214 | 220 |
|
215 | 221 |
my $attachment_name = $self->requirement_spec->type->description . ' ' . ($self->requirement_spec->working_copy_id || $self->requirement_spec->id); |
... | ... | |
255 | 261 |
|
256 | 262 |
$::auth->assert('sales_quotation_edit'); |
257 | 263 |
$::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec); |
258 |
$::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu client_js requirement_spec);
|
|
264 |
$::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu requirement_spec); |
|
259 | 265 |
$self->init_visible_section; |
260 | 266 |
|
261 | 267 |
return 1; |
... | ... | |
469 | 475 |
->jstree->select_node('#tree', '#fb-' . $section->id); |
470 | 476 |
} |
471 | 477 |
|
478 |
sub prepare_pictures_for_printing { |
|
479 |
my ($self) = @_; |
|
480 |
|
|
481 |
my @files; |
|
482 |
my $userspath = File::Spec->rel2abs($::lx_office_conf{paths}->{userspath}); |
|
483 |
my $target = "${userspath}/kivitendo-print-requirement-spec-picture-" . Common::unique_id() . '-'; |
|
484 |
|
|
485 |
foreach my $picture (map { @{ $_->pictures } } @{ $self->requirement_spec->text_blocks }) { |
|
486 |
my $output_file_name = $target . $picture->id . '.' . $picture->get_default_file_name_extension; |
|
487 |
$picture->{print_file_name} = File::Spec->abs2rel($output_file_name, $userspath); |
|
488 |
my $out = IO::File->new($output_file_name, 'w') || die("Could not create file " . $output_file_name); |
|
489 |
$out->binmode; |
|
490 |
$out->print($picture->picture_content); |
|
491 |
$out->close; |
|
492 |
|
|
493 |
push @files, $output_file_name; |
|
494 |
} |
|
495 |
|
|
496 |
return @files; |
|
497 |
} |
|
498 |
|
|
472 | 499 |
1; |
SL/Controller/RequirementSpecTextBlock.pm | ||
---|---|---|
12 | 12 |
use SL::Clipboard; |
13 | 13 |
use SL::Controller::Helper::RequirementSpec; |
14 | 14 |
use SL::DB::RequirementSpec; |
15 |
use SL::DB::RequirementSpecPicture; |
|
15 | 16 |
use SL::DB::RequirementSpecPredefinedText; |
16 | 17 |
use SL::DB::RequirementSpecTextBlock; |
17 | 18 |
use SL::Helper::Flash; |
... | ... | |
19 | 20 |
|
20 | 21 |
use Rose::Object::MakeMethods::Generic |
21 | 22 |
( |
22 |
scalar => [ qw(text_block) ], |
|
23 |
scalar => [ qw(text_block picture) ],
|
|
23 | 24 |
'scalar --get_set_init' => [ qw(predefined_texts js) ], |
24 | 25 |
); |
25 | 26 |
|
26 |
__PACKAGE__->run_before('load_requirement_spec_text_block', only => [qw(ajax_edit ajax_update ajax_delete ajax_flag dragged_and_dropped ajax_copy)]); |
|
27 |
__PACKAGE__->run_before('load_requirement_spec_text_block', only => [qw(ajax_edit ajax_update ajax_delete ajax_flag dragged_and_dropped ajax_copy ajax_add_picture)]);
|
|
27 | 28 |
|
28 | 29 |
# |
29 | 30 |
# actions |
... | ... | |
259 | 260 |
->render($self); |
260 | 261 |
} |
261 | 262 |
|
263 |
# |
|
264 |
# actions for pictures |
|
265 |
# |
|
266 |
|
|
267 |
sub action_ajax_add_picture { |
|
268 |
my ($self) = @_; |
|
269 |
|
|
270 |
$self->picture(SL::DB::RequirementSpecPicture->new); |
|
271 |
$self->render('requirement_spec_text_block/_picture_form', { layout => 0 }); |
|
272 |
} |
|
273 |
|
|
274 |
sub action_ajax_edit_picture { |
|
275 |
my ($self) = @_; |
|
276 |
|
|
277 |
$self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{picture_id})->load); |
|
278 |
$self->text_block($self->picture->text_block); |
|
279 |
$self->render('requirement_spec_text_block/_picture_form', { layout => 0 }); |
|
280 |
} |
|
281 |
|
|
282 |
sub action_ajax_create_picture { |
|
283 |
my ($self, %params) = @_; |
|
284 |
|
|
285 |
my $attributes = $::form->{ $::form->{form_prefix} } || die "Missing attributes"; |
|
286 |
$attributes->{picture_file_name} = ((($::form->{ATTACHMENTS} || {})->{ $::form->{form_prefix} } || {})->{picture_content} || {})->{filename}; |
|
287 |
my @errors = $self->picture(SL::DB::RequirementSpecPicture->new(%{ $attributes }))->validate; |
|
288 |
|
|
289 |
return $self->js->error(@errors)->render($self) if @errors; |
|
290 |
|
|
291 |
$self->picture->save; |
|
292 |
|
|
293 |
$self->text_block($self->picture->text_block); |
|
294 |
my $html = $self->render('requirement_spec_text_block/_text_block_picture', { output => 0 }, picture => $self->picture); |
|
295 |
|
|
296 |
$self->invalidate_version |
|
297 |
->dialog->close('#jqueryui_popup_dialog') |
|
298 |
->append('#text-block-' . $self->text_block->id . '-pictures', $html) |
|
299 |
->show('#text-block-' . $self->text_block->id . '-pictures') |
|
300 |
->render($self); |
|
301 |
} |
|
302 |
|
|
303 |
sub action_ajax_update_picture { |
|
304 |
my ($self) = @_; |
|
305 |
|
|
306 |
my $attributes = $::form->{ $::form->{form_prefix} } || die "Missing attributes"; |
|
307 |
$self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load); |
|
308 |
|
|
309 |
if (!$attributes->{picture_content}) { |
|
310 |
delete $attributes->{picture_content}; |
|
311 |
} else { |
|
312 |
$attributes->{picture_file_name} = ((($::form->{ATTACHMENTS} || {})->{ $::form->{form_prefix} } || {})->{picture_content} || {})->{filename}; |
|
313 |
} |
|
314 |
|
|
315 |
$self->picture->assign_attributes(%{ $attributes }); |
|
316 |
my @errors = $self->picture->validate; |
|
317 |
|
|
318 |
return $self->js->error(@errors)->render($self) if @errors; |
|
319 |
|
|
320 |
$self->picture->save; |
|
321 |
|
|
322 |
$self->text_block($self->picture->text_block); |
|
323 |
my $html = $self->render('requirement_spec_text_block/_text_block_picture', { output => 0 }, picture => $self->picture); |
|
324 |
|
|
325 |
$self->invalidate_version |
|
326 |
->dialog->close('#jqueryui_popup_dialog') |
|
327 |
->replaceWith('#text-block-picture-' . $self->picture->id, $html) |
|
328 |
->show('#text-block-' . $self->text_block->id . '-pictures') |
|
329 |
->render($self); |
|
330 |
} |
|
331 |
|
|
332 |
sub action_ajax_delete_picture { |
|
333 |
my ($self) = @_; |
|
334 |
|
|
335 |
$self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load); |
|
336 |
$self->picture->delete; |
|
337 |
$self->text_block(SL::DB::RequirementSpecTextBlock->new(id => $self->picture->text_block_id)->load); |
|
338 |
|
|
339 |
$self->invalidate_version |
|
340 |
->remove('#text-block-picture-' . $self->picture->id) |
|
341 |
->action_if(!@{ $self->text_block->pictures }, 'hide', '#text-block-' . $self->text_block->id . '-pictures') |
|
342 |
->render($self); |
|
343 |
} |
|
344 |
|
|
345 |
sub action_ajax_download_picture { |
|
346 |
my ($self) = @_; |
|
347 |
|
|
348 |
$self->picture(SL::DB::RequirementSpecPicture->new(id => $::form->{id})->load); |
|
349 |
$self->send_file(\$self->picture->{picture_content}, type => $self->picture->picture_content_type, name => $self->picture->picture_file_name); |
|
350 |
} |
|
351 |
|
|
262 | 352 |
# |
263 | 353 |
# filters |
264 | 354 |
# |
SL/DB/Helper/ALL.pm | ||
---|---|---|
82 | 82 |
use SL::DB::RequirementSpecDependency; |
83 | 83 |
use SL::DB::RequirementSpecItem; |
84 | 84 |
use SL::DB::RequirementSpecOrder; |
85 |
use SL::DB::RequirementSpecPicture; |
|
85 | 86 |
use SL::DB::RequirementSpecPredefinedText; |
86 | 87 |
use SL::DB::RequirementSpecRisk; |
87 | 88 |
use SL::DB::RequirementSpecStatus; |
SL/DB/Helper/Mappings.pm | ||
---|---|---|
162 | 162 |
requirement_spec_item_dependencies => 'RequirementSpecDependency', |
163 | 163 |
requirement_spec_items => 'RequirementSpecItem', |
164 | 164 |
requirement_spec_orders => 'RequirementSpecOrder', |
165 |
requirement_spec_pictures => 'RequirementSpecPicture', |
|
165 | 166 |
requirement_spec_predefined_texts => 'RequirementSpecPredefinedText', |
166 | 167 |
requirement_spec_risks => 'RequirementSpecRisk', |
167 | 168 |
requirement_spec_statuses => 'RequirementSpecStatus', |
SL/DB/Manager/RequirementSpecPicture.pm | ||
---|---|---|
1 |
# This file has been auto-generated only because it didn't exist. |
|
2 |
# Feel free to modify it at will; it will not be overwritten automatically. |
|
3 |
|
|
4 |
package SL::DB::Manager::RequirementSpecPicture; |
|
5 |
|
|
6 |
use strict; |
|
7 |
|
|
8 |
use SL::DB::Helper::Manager; |
|
9 |
use base qw(SL::DB::Helper::Manager); |
|
10 |
|
|
11 |
sub object_class { 'SL::DB::RequirementSpecPicture' } |
|
12 |
|
|
13 |
__PACKAGE__->make_manager_methods; |
|
14 |
|
|
15 |
1; |
SL/DB/MetaSetup/RequirementSpec.pm | ||
---|---|---|
16 | 16 |
itime => { type => 'timestamp', default => 'now()' }, |
17 | 17 |
mtime => { type => 'timestamp' }, |
18 | 18 |
previous_fb_number => { type => 'integer', not_null => 1 }, |
19 |
previous_picture_number => { type => 'integer', default => '0', not_null => 1 }, |
|
19 | 20 |
previous_section_number => { type => 'integer', not_null => 1 }, |
20 | 21 |
project_id => { type => 'integer' }, |
21 | 22 |
status_id => { type => 'integer' }, |
SL/DB/MetaSetup/RequirementSpecPicture.pm | ||
---|---|---|
1 |
# This file has been auto-generated. Do not modify it; it will be overwritten |
|
2 |
# by rose_auto_create_model.pl automatically. |
|
3 |
package SL::DB::RequirementSpecPicture; |
|
4 |
|
|
5 |
use strict; |
|
6 |
|
|
7 |
use base qw(SL::DB::Object); |
|
8 |
|
|
9 |
__PACKAGE__->meta->table('requirement_spec_pictures'); |
|
10 |
|
|
11 |
__PACKAGE__->meta->columns( |
|
12 |
description => { type => 'text' }, |
|
13 |
id => { type => 'serial', not_null => 1 }, |
|
14 |
itime => { type => 'timestamp', default => 'now()', not_null => 1 }, |
|
15 |
mtime => { type => 'timestamp' }, |
|
16 |
number => { type => 'text', not_null => 1 }, |
|
17 |
picture_content => { type => 'bytea', not_null => 1 }, |
|
18 |
picture_content_type => { type => 'text', not_null => 1 }, |
|
19 |
picture_file_name => { type => 'text', not_null => 1 }, |
|
20 |
picture_height => { type => 'integer', not_null => 1 }, |
|
21 |
picture_mtime => { type => 'timestamp', default => 'now()', not_null => 1 }, |
|
22 |
picture_width => { type => 'integer', not_null => 1 }, |
|
23 |
position => { type => 'integer', not_null => 1 }, |
|
24 |
requirement_spec_id => { type => 'integer', not_null => 1 }, |
|
25 |
text_block_id => { type => 'integer', not_null => 1 }, |
|
26 |
thumbnail_content => { type => 'bytea', not_null => 1 }, |
|
27 |
thumbnail_content_type => { type => 'text', not_null => 1 }, |
|
28 |
thumbnail_height => { type => 'integer', not_null => 1 }, |
|
29 |
thumbnail_width => { type => 'integer', not_null => 1 }, |
|
30 |
); |
|
31 |
|
|
32 |
__PACKAGE__->meta->primary_key_columns([ 'id' ]); |
|
33 |
|
|
34 |
__PACKAGE__->meta->allow_inline_column_values(1); |
|
35 |
|
|
36 |
__PACKAGE__->meta->foreign_keys( |
|
37 |
requirement_spec => { |
|
38 |
class => 'SL::DB::RequirementSpec', |
|
39 |
key_columns => { requirement_spec_id => 'id' }, |
|
40 |
}, |
|
41 |
|
|
42 |
text_block => { |
|
43 |
class => 'SL::DB::RequirementSpecTextBlock', |
|
44 |
key_columns => { text_block_id => 'id' }, |
|
45 |
}, |
|
46 |
); |
|
47 |
|
|
48 |
1; |
|
49 |
; |
SL/DB/RequirementSpec.pm | ||
---|---|---|
60 | 60 |
sub _before_save_initialize_not_null_columns { |
61 | 61 |
my ($self) = @_; |
62 | 62 |
|
63 |
$self->previous_section_number(0) if !defined $self->previous_section_number; |
|
64 |
$self->previous_fb_number(0) if !defined $self->previous_fb_number; |
|
63 |
for (qw(previous_section_number previous_fb_number previous_picture_number)) { |
|
64 |
$self->$_(0) if !defined $self->$_; |
|
65 |
} |
|
65 | 66 |
|
66 | 67 |
return 1; |
67 | 68 |
} |
... | ... | |
149 | 150 |
|
150 | 151 |
# Copy attributes. |
151 | 152 |
if (!$params->{paste_template}) { |
152 |
$self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate time_estimation previous_section_number previous_fb_number is_template)), |
|
153 |
$self->assign_attributes(map({ ($_ => $source->$_) } qw(type_id status_id customer_id project_id title hourly_rate time_estimation previous_section_number previous_fb_number previous_picture_number is_template)),
|
|
153 | 154 |
%attributes); |
154 | 155 |
} |
155 | 156 |
|
156 | 157 |
my %paste_template_result; |
157 | 158 |
|
158 |
# Clone text blocks. |
|
159 |
# Clone text blocks and pictures. |
|
160 |
my $clone_picture = sub { |
|
161 |
my ($picture) = @_; |
|
162 |
my $cloned = Rose::DB::Object::Helpers::clone_and_reset($picture); |
|
163 |
$cloned->position(undef); |
|
164 |
return $cloned; |
|
165 |
}; |
|
166 |
|
|
159 | 167 |
my $clone_text_block = sub { |
160 | 168 |
my ($text_block) = @_; |
161 | 169 |
my $cloned = Rose::DB::Object::Helpers::clone_and_reset($text_block); |
162 | 170 |
$cloned->position(undef); |
171 |
$cloned->pictures([ map { $clone_picture->($_) } @{ $text_block->pictures_sorted } ]); |
|
163 | 172 |
return $cloned; |
164 | 173 |
}; |
165 | 174 |
|
SL/DB/RequirementSpecPicture.pm | ||
---|---|---|
1 |
package SL::DB::RequirementSpecPicture; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use Carp; |
|
6 |
use GD; |
|
7 |
use Image::Info; |
|
8 |
use List::MoreUtils qw(apply); |
|
9 |
use List::Util qw(max); |
|
10 |
use Rose::DB::Object::Util; |
|
11 |
use SL::DB::MetaSetup::RequirementSpecPicture; |
|
12 |
use SL::DB::Manager::RequirementSpecPicture; |
|
13 |
use SL::DB::Helper::ActsAsList; |
|
14 |
use SL::Locale::String; |
|
15 |
|
|
16 |
__PACKAGE__->meta->initialize; |
|
17 |
|
|
18 |
__PACKAGE__->configure_acts_as_list(group_by => [qw(requirement_spec_id text_block_id)]); |
|
19 |
|
|
20 |
__PACKAGE__->before_save(\&_create_picture_number); |
|
21 |
__PACKAGE__->before_save(\&_update_thumbnail); |
|
22 |
|
|
23 |
our %supported_mime_types = ( |
|
24 |
'image/gif' => { extension => 'gif', convert_to_png => 1, }, |
|
25 |
'image/png' => { extension => 'png' }, |
|
26 |
'image/jpeg' => { extension => 'jpg' }, |
|
27 |
); |
|
28 |
|
|
29 |
sub _create_picture_number { |
|
30 |
my ($self) = @_; |
|
31 |
|
|
32 |
return 1 if $self->number; |
|
33 |
return 0 if !$self->requirement_spec_id; |
|
34 |
|
|
35 |
my $next_number = $self->requirement_spec->previous_picture_number + 1; |
|
36 |
|
|
37 |
$self->requirement_spec->update_attributes(previous_picture_number => $next_number) || return 0; |
|
38 |
|
|
39 |
$self->number($next_number); |
|
40 |
|
|
41 |
return 1; |
|
42 |
} |
|
43 |
|
|
44 |
sub _update_thumbnail { |
|
45 |
my ($self) = @_; |
|
46 |
|
|
47 |
return 1 if !$self->picture_content || !$self->picture_content_type || !Rose::DB::Object::Util::get_column_value_modified($self, 'picture_content'); |
|
48 |
$self->create_thumbnail; |
|
49 |
return 1; |
|
50 |
} |
|
51 |
|
|
52 |
sub create_thumbnail { |
|
53 |
my ($self) = @_; |
|
54 |
|
|
55 |
croak "No picture set yet" if !$self->picture_content; |
|
56 |
|
|
57 |
my $image = GD::Image->new($self->picture_content); |
|
58 |
my ($width, $height) = $image->getBounds; |
|
59 |
my $max_dim = 64; |
|
60 |
my $curr_max = max $width, $height, 1; |
|
61 |
my $factor = $curr_max <= $max_dim ? 1 : $curr_max / $max_dim; |
|
62 |
my $new_width = int($width / $factor + 0.5); |
|
63 |
my $new_height = int($height / $factor + 0.5); |
|
64 |
my $thumbnail = GD::Image->new($new_width, $new_height); |
|
65 |
|
|
66 |
$thumbnail->copyResized($image, 0, 0, 0, 0, $new_width, $new_height, $width, $height); |
|
67 |
|
|
68 |
$self->thumbnail_content($thumbnail->png); |
|
69 |
$self->thumbnail_content_type('image/png'); |
|
70 |
$self->thumbnail_width($new_width); |
|
71 |
$self->thumbnail_height($new_height); |
|
72 |
|
|
73 |
return 1; |
|
74 |
} |
|
75 |
|
|
76 |
sub update_type_and_dimensions { |
|
77 |
my ($self) = @_; |
|
78 |
|
|
79 |
return () if !$self->picture_content; |
|
80 |
return () if $self->picture_content_type && $self->picture_width && $self->picture_height && !Rose::DB::Object::Util::get_column_value_modified($self, 'picture_content'); |
|
81 |
|
|
82 |
my @errors = $self->probe_type; |
|
83 |
return @errors if @errors; |
|
84 |
|
|
85 |
my $info = $supported_mime_types{ $self->picture_content_type }; |
|
86 |
if ($info->{convert_to_png}) { |
|
87 |
$self->picture_content(GD::Image->new($self->picture_content)->png); |
|
88 |
$self->picture_content_type('image/png'); |
|
89 |
$self->picture_file_name(apply { s/\.[^\.]+$//; $_ .= '.png'; } $self->picture_file_name); |
|
90 |
} |
|
91 |
|
|
92 |
return (); |
|
93 |
} |
|
94 |
|
|
95 |
sub probe_type { |
|
96 |
my ($self) = @_; |
|
97 |
|
|
98 |
return (t8("No picture uploaded yet")) if !$self->picture_content; |
|
99 |
|
|
100 |
my $info = Image::Info::image_info(\$self->{picture_content}); |
|
101 |
if (!$info || $info->{error} || !$info->{file_media_type} || !$supported_mime_types{ $info->{file_media_type} }) { |
|
102 |
$::lxdebug->warn("Image::Info error: " . $info->{error}) if $info && $info->{error}; |
|
103 |
return (t8('Unsupported image type (supported types: #1)', join(' ', sort keys %supported_mime_types))); |
|
104 |
} |
|
105 |
|
|
106 |
$self->picture_content_type($info->{file_media_type}); |
|
107 |
$self->picture_width($info->{width}); |
|
108 |
$self->picture_height($info->{height}); |
|
109 |
$self->picture_mtime(DateTime->now_local); |
|
110 |
|
|
111 |
$self->create_thumbnail; |
|
112 |
|
|
113 |
return (); |
|
114 |
} |
|
115 |
|
|
116 |
sub get_default_file_name_extension { |
|
117 |
my ($self) = @_; |
|
118 |
|
|
119 |
my $info = $supported_mime_types{ $self->picture_content_type } || croak("Unsupported content type " . $self->picture_content_type); |
|
120 |
return $info->{extension}; |
|
121 |
} |
|
122 |
|
|
123 |
sub validate { |
|
124 |
my ($self) = @_; |
|
125 |
|
|
126 |
my @errors; |
|
127 |
|
|
128 |
push @errors, t8('The file name is missing') if !$self->picture_file_name; |
|
129 |
|
|
130 |
if (!length($self->picture_content // '')) { |
|
131 |
push @errors, t8('No picture has been uploaded'); |
|
132 |
|
|
133 |
} else { |
|
134 |
push @errors, $self->update_type_and_dimensions; |
|
135 |
} |
|
136 |
|
|
137 |
return @errors; |
|
138 |
} |
|
139 |
|
|
140 |
1; |
SL/DB/RequirementSpecTextBlock.pm | ||
---|---|---|
2 | 2 |
|
3 | 3 |
use strict; |
4 | 4 |
|
5 |
use Carp; |
|
5 | 6 |
use List::MoreUtils qw(any); |
6 | 7 |
use Rose::DB::Object::Helpers; |
7 | 8 |
use Rose::DB::Object::Util; |
... | ... | |
11 | 12 |
use SL::DB::Helper::ActsAsList; |
12 | 13 |
use SL::Locale::String; |
13 | 14 |
|
15 |
__PACKAGE__->meta->add_relationship( |
|
16 |
pictures => { |
|
17 |
type => 'one to many', |
|
18 |
class => 'SL::DB::RequirementSpecPicture', |
|
19 |
column_map => { id => 'text_block_id' }, |
|
20 |
}, |
|
21 |
); |
|
22 |
|
|
14 | 23 |
__PACKAGE__->meta->initialize; |
15 | 24 |
|
16 | 25 |
__PACKAGE__->configure_acts_as_list(group_by => [qw(requirement_spec_id output_position)]); |
... | ... | |
48 | 57 |
return 1; |
49 | 58 |
} |
50 | 59 |
|
60 |
sub pictures_sorted { |
|
61 |
my ($self, @args) = @_; |
|
62 |
|
|
63 |
croak "Not a writer" if @args; |
|
64 |
|
|
65 |
return [ sort { $a->position <=> $b->position } $self->pictures ]; |
|
66 |
} |
|
67 |
|
|
51 | 68 |
1; |
SL/InstallationCheck.pm | ||
---|---|---|
27 | 27 |
{ name => "Email::MIME", url => "http://search.cpan.org/~rjbs/", debian => 'libemail-mime-perl' }, |
28 | 28 |
{ name => "FCGI", version => '0.72', url => "http://search.cpan.org/~mstrout/", debian => 'libfcgi-perl' }, |
29 | 29 |
{ name => "File::Copy::Recursive", url => "http://search.cpan.org/~dmuey/", debian => 'libfile-copy-recursive-perl' }, |
30 |
{ name => "GD", url => "http://search.cpan.org/~lds/", debian => 'libgd-gd2-perl', }, |
|
31 |
{ name => "Image::Info", url => "http://search.cpan.org/~srezic/", debian => 'libimage-info-perl' }, |
|
30 | 32 |
{ name => "JSON", url => "http://search.cpan.org/~makamaka", debian => 'libjson-perl' }, |
31 | 33 |
{ name => "List::MoreUtils", version => '0.21', url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' }, |
32 | 34 |
{ name => "Params::Validate", url => "http://search.cpan.org/~drolsky/", debian => 'libparams-validate-perl' }, |
SL/Template/Plugin/Base64.pm | ||
---|---|---|
1 |
package SL::Template::Plugin::Base64; |
|
2 |
|
|
3 |
use strict; |
|
4 |
use vars qw($VERSION); |
|
5 |
|
|
6 |
$VERSION = 0.01; |
|
7 |
|
|
8 |
use base qw(Template::Plugin); |
|
9 |
use Template::Plugin; |
|
10 |
use Template::Stash; |
|
11 |
use MIME::Base64 (); |
|
12 |
|
|
13 |
$Template::Stash::SCALAR_OPS->{'encode_base64'} = \&_encode_base64; |
|
14 |
$Template::Stash::SCALAR_OPS->{'decode_base64'} = \&_decode_base64; |
|
15 |
|
|
16 |
sub new { |
|
17 |
my ($class, $context, $options) = @_; |
|
18 |
|
|
19 |
$context->define_filter('encode_base64', \&_encode_base64); |
|
20 |
$context->define_filter('decode_base64', \&_decode_base64); |
|
21 |
return bless {}, $class; |
|
22 |
} |
|
23 |
|
|
24 |
sub _encode_base64 { |
|
25 |
return MIME::Base64::encode_base64(shift, ''); |
|
26 |
} |
|
27 |
|
|
28 |
sub _decode_base64 { |
|
29 |
my ($self, $var) = @_; |
|
30 |
return MIME::Base64::decode_base64(shift); |
|
31 |
} |
|
32 |
|
|
33 |
1; |
|
34 |
|
|
35 |
__END__ |
|
36 |
|
|
37 |
=head1 NAME |
|
38 |
|
|
39 |
SL::Template::Plugin::Base64 - TT2 interface to base64 encoding/decoding |
|
40 |
|
|
41 |
=head1 SYNOPSIS |
|
42 |
|
|
43 |
[% USE Base64 -%] |
|
44 |
[% SELF.some_object.binary_stuff.encode_base64 -%] |
|
45 |
[% SELF.some_object.binary_stuff FILTER encode_base64 -%] |
|
46 |
|
|
47 |
=head1 DESCRIPTION |
|
48 |
|
|
49 |
The I<Base64> Template Toolkit plugin provides access to the Base64 |
|
50 |
routines from L<MIME::Base64>. |
|
51 |
|
|
52 |
The following filters (and vmethods of the same name) are installed |
|
53 |
into the current context: |
|
54 |
|
|
55 |
=over 4 |
|
56 |
|
|
57 |
=item C<encode_base64> |
|
58 |
|
|
59 |
Returns the string encoded as Base64. |
|
60 |
|
|
61 |
=item C<decode_base64> |
|
62 |
|
|
63 |
Returns the Base64 string decoded back to binary. |
|
64 |
|
|
65 |
=back |
|
66 |
|
|
67 |
As the filters are also available as vmethods the following are all |
|
68 |
equivalent: |
|
69 |
|
|
70 |
FILTER encode_base64; content; END; |
|
71 |
content FILTER encode_base64; |
|
72 |
content.encode_base64; |
|
73 |
|
|
74 |
=head1 AUTHOR |
|
75 |
|
|
76 |
Moritz Bunkus E<lt>m.bunkus@linet-services.de<gt> |
|
77 |
|
|
78 |
=cut |
css/requirement_spec.css | ||
---|---|---|
48 | 48 |
.context-menu-item.icon-save { background-image: url("../image/document-save.png"); } |
49 | 49 |
.context-menu-item.icon-revert { background-image: url("../image/edit-undo.png"); } |
50 | 50 |
.context-menu-item.icon-pdf { background-image: url("../image/application-pdf.png"); } |
51 |
.context-menu-item.icon-add-picture { background-image: url("../image/add-picture.png"); } |
|
52 |
.context-menu-item.icon-download { background-image: url("../image/download.png"); } |
|
51 | 53 |
|
52 | 54 |
/* ------------------------------------------------------------ */ |
53 | 55 |
/* Sections & function blocks */ |
... | ... | |
158 | 160 |
margin-left: 0; |
159 | 161 |
} |
160 | 162 |
|
163 |
.requirement-spec-text-block-picture-thumbnail { |
|
164 |
border-radius: 5px; |
|
165 |
border: 2px solid #ebebeb; |
|
166 |
float: left; |
|
167 |
margin-right: 20px; |
|
168 |
padding: 5px; |
|
169 |
text-align: center; |
|
170 |
width: 130px; |
|
171 |
} |
|
172 |
|
|
173 |
.requirement-spec-text-block-picture-thumbnail-img-container { |
|
174 |
height: 64px; |
|
175 |
margin: auto; |
|
176 |
padding: auto; |
|
177 |
width: 64px; |
|
178 |
} |
|
179 |
|
|
180 |
.requirement-spec-text-block-picture-thumbnail.selected { |
|
181 |
border: 2px solid #cbb120; |
|
182 |
} |
|
183 |
|
|
161 | 184 |
/* ------------------------------------------------------------ */ |
162 | 185 |
/* Time/cost estimation */ |
163 | 186 |
/* ------------------------------------------------------------ */ |
js/locale/de.js | ||
---|---|---|
1 | 1 |
namespace("kivi").setupLocale({ |
2 | 2 |
"Add function block":"Funktionsblock hinzufügen", |
3 | 3 |
"Add linked record":"Verknüpften Beleg hinzufügen", |
4 |
"Add picture":"Bild hinzufügen", |
|
5 |
"Add picture to text block":"Bild dem Textblock hinzufügen", |
|
4 | 6 |
"Add section":"Abschnitt hinzufügen", |
5 | 7 |
"Add sub function block":"Unterfunktionsblock hinzufügen", |
6 | 8 |
"Add text block":"Textblock erfassen", |
... | ... | |
18 | 20 |
"Create new version":"Neue Version anlegen", |
19 | 21 |
"Database Connection Test":"Test der Datenbankverbindung", |
20 | 22 |
"Delete":"Löschen", |
23 |
"Delete picture":"Bild löschen", |
|
21 | 24 |
"Delete quotation/order":"Angebot/Auftrag löschen", |
22 | 25 |
"Delete requirement spec":"Pflichtenheft löschen", |
23 | 26 |
"Delete template":"Vorlage löschen", |
... | ... | |
25 | 28 |
"Do you really want to cancel?":"Wollen Sie wirklich abbrechen?", |
26 | 29 |
"Do you really want to revert to this version?":"Wollen Sie wirklich auf diese Version zurücksetzen?", |
27 | 30 |
"Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"Soll die Kontonummer \"#1\" zu \"#2\" und den Name \"#3\" zu \"#4\" geändert werden?", |
31 |
"Download picture":"Bild herunterladen", |
|
28 | 32 |
"Edit":"Bearbeiten", |
29 | 33 |
"Edit article/section assignments":"Zuweisung Artikel/Abschnitte bearbeiten", |
34 |
"Edit picture":"Bild bearbeiten", |
|
30 | 35 |
"Edit text block":"Textblock bearbeiten", |
31 | 36 |
"Enter longdescription":"Langtext eingeben", |
32 | 37 |
"Function block actions":"Funktionsblockaktionen", |
33 | 38 |
"If you switch to a different tab without saving you will lose the data you've entered in the current tab.":"Wenn Sie auf einen anderen Tab wechseln, ohne vorher zu speichern, so gehen die im aktuellen Tab eingegebenen Daten verloren.", |
34 | 39 |
"Map":"Karte", |
35 |
"Orders/Quotations actions":"", |
|
36 | 40 |
"Part picker":"Artikelauswahl", |
37 | 41 |
"Paste":"Einfügen", |
38 | 42 |
"Paste template":"Vorlage einfügen", |
... | ... | |
44 | 48 |
"Section/Function block actions":"Abschnitts-/Funktionsblockaktionen", |
45 | 49 |
"Select template to paste":"Einzufügende Vorlage auswählen", |
46 | 50 |
"Text block actions":"Textblockaktionen", |
51 |
"Text block picture actions":"Aktionen für Textblockbilder", |
|
47 | 52 |
"The description is missing.":"Die Beschreibung fehlt.", |
48 | 53 |
"The name is missing.":"Der Name fehlt.", |
49 | 54 |
"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.", |
js/requirement_spec.js | ||
---|---|---|
108 | 108 |
|
109 | 109 |
ns.find_text_block_id = function(clicked_elt) { |
110 | 110 |
var id = $(clicked_elt).attr('id'); |
111 |
|
|
112 |
if (/^text-block-picture-\d+$/.test(id)) |
|
113 |
id = $(clicked_elt).closest('.text-block-context-menu').attr('id'); |
|
114 |
|
|
111 | 115 |
// console.log("id: " + id); |
112 | 116 |
if (/^text-block-\d+$/.test(id)) { |
113 | 117 |
// console.log("find_text_block_id: case 1: " + id.substr(11)); |
... | ... | |
183 | 187 |
} |
184 | 188 |
}; |
185 | 189 |
|
190 |
ns.find_text_block_picture_id = function(clicked_elt) { |
|
191 |
var id = $(clicked_elt).attr('id'); |
|
192 |
var match = id.match(/^text-block-picture-(\d+)$/); |
|
193 |
if (match) |
|
194 |
return match[1] * 1; |
|
195 |
|
|
196 |
return undefined; |
|
197 |
}; |
|
198 |
|
|
199 |
ns.add_edit_text_block_picture_ajax_call = function(key, opt) { |
|
200 |
var title = key == 'add_picture' ? kivi.t8('Add picture to text block') : kivi.t8('Edit picture'); |
|
201 |
|
|
202 |
kivi.popup_dialog({ url: 'controller.pl', |
|
203 |
data: { action: 'RequirementSpecTextBlock/ajax_' + key, |
|
204 |
id: ns.find_text_block_id(opt.$trigger), |
|
205 |
picture_id: ns.find_text_block_picture_id(opt.$trigger) }, |
|
206 |
dialog: { title: title }}); |
|
207 |
|
|
208 |
return true; |
|
209 |
}; |
|
210 |
|
|
211 |
ns.standard_text_block_picture_ajax_call = function(key, opt) { |
|
212 |
var data = { |
|
213 |
action: "RequirementSpecTextBlock/ajax_" + key |
|
214 |
, id: ns.find_text_block_picture_id(opt.$trigger) |
|
215 |
}; |
|
216 |
|
|
217 |
if (key == 'download_picture') |
|
218 |
$.download("controller.pl", data); |
|
219 |
else |
|
220 |
$.post("controller.pl", data, kivi.eval_json_result); |
|
221 |
|
|
222 |
return true; |
|
223 |
}; |
|
224 |
|
|
225 |
ns.ask_delete_text_block_picture = function(key, opt) { |
|
226 |
if (confirm(kivi.t8("Are you sure?"))) |
|
227 |
ns.standard_text_block_picture_ajax_call(key, opt); |
|
228 |
return true; |
|
229 |
}; |
|
230 |
|
|
231 |
ns.handle_text_block_picture_popup_menu_markings = function(opt, add) { |
|
232 |
var id = ns.find_text_block_picture_id(opt.$trigger); |
|
233 |
if (id) |
|
234 |
$('#text-block-picture-' + id ).toggleClass('selected', add); |
|
235 |
return true; |
|
236 |
}; |
|
237 |
|
|
238 |
ns.text_block_picture_popup_menu_shown = function(opt) { |
|
239 |
return ns.handle_text_block_picture_popup_menu_markings(opt, true); |
|
240 |
}; |
|
241 |
|
|
242 |
ns.text_block_picture_popup_menu_hidden = function(opt) { |
|
243 |
return ns.handle_text_block_picture_popup_menu_markings(opt, false); |
|
244 |
}; |
|
245 |
|
|
186 | 246 |
// -------------------------------------------------------------------------------- |
187 | 247 |
// ------------------------------ sections and items ------------------------------ |
188 | 248 |
// -------------------------------------------------------------------------------- |
... | ... | |
568 | 628 |
}; |
569 | 629 |
} // if (is_template) ... else ... |
570 | 630 |
|
571 |
var events = { |
|
572 |
show: kivi.requirement_spec.text_block_popup_menu_shown |
|
573 |
, hide: kivi.requirement_spec.text_block_popup_menu_hidden |
|
574 |
}; |
|
575 |
|
|
576 | 631 |
$.contextMenu({ |
577 | 632 |
selector: '.text-block-context-menu', |
578 | 633 |
events: { |
... | ... | |
584 | 639 |
, add: { name: kivi.t8('Add text block'), icon: "add", callback: kivi.requirement_spec.standard_text_block_ajax_call } |
585 | 640 |
, edit: { name: kivi.t8('Edit text block'), icon: "edit", callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands } |
586 | 641 |
, delete: { name: kivi.t8('Delete text block'), icon: "delete", callback: kivi.requirement_spec.ask_delete_text_block, disabled: kivi.requirement_spec.disable_edit_text_block_commands } |
642 |
, add_picture: { name: kivi.t8('Add picture to text block'), icon: "add-picture", callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands } |
|
587 | 643 |
, sep1: "---------" |
588 | 644 |
, flag: { name: kivi.t8('Toggle marker'), icon: "flag", callback: kivi.requirement_spec.standard_text_block_ajax_call, disabled: kivi.requirement_spec.disable_edit_text_block_commands } |
589 | 645 |
, sep2: "---------" |
... | ... | |
592 | 648 |
}, general_actions) |
593 | 649 |
}); |
594 | 650 |
|
651 |
$.contextMenu({ |
|
652 |
selector: '.text-block-picture-context-menu', |
|
653 |
events: { |
|
654 |
show: kivi.requirement_spec.text_block_picture_popup_menu_shown |
|
655 |
, hide: kivi.requirement_spec.text_block_picture_popup_menu_hidden |
|
656 |
}, |
|
657 |
items: $.extend({ |
|
658 |
heading: { name: kivi.t8('Text block picture actions'), className: 'context-menu-heading' } |
|
659 |
, add_picture: { name: kivi.t8('Add picture'), icon: "add-picture", callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call } |
|
660 |
, edit_picture: { name: kivi.t8('Edit picture'), icon: "edit", callback: kivi.requirement_spec.add_edit_text_block_picture_ajax_call } |
|
661 |
, delete_picture: { name: kivi.t8('Delete picture'), icon: "delete", callback: kivi.requirement_spec.ask_delete_text_block_picture } |
|
662 |
, download_picture: { name: kivi.t8('Download picture'), icon: "download", callback: kivi.requirement_spec.standard_text_block_picture_ajax_call } |
|
663 |
}, general_actions) |
|
664 |
}); |
|
665 |
|
|
595 | 666 |
$.contextMenu({ |
596 | 667 |
selector: '.basic-settings-context-menu', |
597 | 668 |
items: $.extend({ |
... | ... | |
671 | 742 |
items: general_actions |
672 | 743 |
}); |
673 | 744 |
|
674 |
events = { |
|
745 |
var events = {
|
|
675 | 746 |
show: kivi.requirement_spec.item_popup_menu_shown |
676 | 747 |
, hide: kivi.requirement_spec.item_popup_menu_hidden |
677 | 748 |
}; |
locale/de/all | ||
---|---|---|
177 | 177 |
'Add new currency' => 'Neue Währung hinzufügen', |
178 | 178 |
'Add new custom variable' => 'Neue benutzerdefinierte Variable erfassen', |
179 | 179 |
'Add note' => 'Notiz erfassen', |
180 |
'Add printer' => 'Drucker hinzufügen', |
|
180 |
'Add picture' => 'Bild hinzufügen', |
|
181 |
'Add picture to text block' => 'Bild dem Textblock hinzufügen', |
|
181 | 182 |
'Add section' => 'Abschnitt hinzufügen', |
182 | 183 |
'Add sub function block' => 'Unterfunktionsblock hinzufügen', |
183 | 184 |
'Add text block' => 'Textblock erfassen', |
... | ... | |
631 | 632 |
'Current Earnings' => 'Gewinn', |
632 | 633 |
'Current assets account' => 'Konto für Umlaufvermögen', |
633 | 634 |
'Current filter' => 'Aktueller Filter', |
635 |
'Current picture' => 'Aktuelles Bild', |
|
634 | 636 |
'Current profile' => 'Aktuelles Profil', |
635 | 637 |
'Current status' => 'Aktueller Status', |
636 | 638 |
'Current value:' => 'Aktueller Wert:', |
... | ... | |
742 | 744 |
'Delete Shipto' => 'Lieferadresse löschen', |
743 | 745 |
'Delete drafts' => 'Entwürfe löschen', |
744 | 746 |
'Delete links' => 'Verknüpfungen löschen', |
747 |
'Delete picture' => 'Bild löschen', |
|
745 | 748 |
'Delete profile' => 'Profil löschen', |
746 | 749 |
'Delete quotation/order' => 'Angebot/Auftrag löschen', |
747 | 750 |
'Delete requirement spec' => 'Pflichtenheft löschen', |
... | ... | |
789 | 792 |
'Detail view' => 'Detailanzeige', |
790 | 793 |
'Details (one letter abbreviation)' => 'D', |
791 | 794 |
'Difference' => 'Differenz', |
795 |
'Dimensions' => 'Abmessungen', |
|
792 | 796 |
'Directory' => 'Verzeichnis', |
793 | 797 |
'Discard duplicate entries in CSV file' => 'Doppelte Einträge in CSV-Datei verwerfen', |
794 | 798 |
'Discard entries with duplicates in database or CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank oder der CSV-Datei gibt', |
... | ... | |
828 | 832 |
'Done' => 'Fertig', |
829 | 833 |
'Double partnumbers' => 'Doppelte Artikelnummern', |
830 | 834 |
'Download SEPA XML export file' => 'SEPA-XML-Exportdatei herunterladen', |
835 |
'Download picture' => 'Bild herunterladen', |
|
831 | 836 |
'Download sample file' => 'Beispieldatei herunterladen', |
832 | 837 |
'Draft saved.' => 'Entwurf gespeichert.', |
833 | 838 |
'Drawing' => 'Zeichnung', |
... | ... | |
923 | 928 |
'Edit greetings' => 'Anreden bearbeiten', |
924 | 929 |
'Edit note' => 'Notiz bearbeiten', |
925 | 930 |
'Edit payment term' => 'Zahlungsbedingungen bearbeiten', |
931 |
'Edit picture' => 'Bild bearbeiten', |
|
926 | 932 |
'Edit predefined text' => 'Vordefinierten Textblock bearbeiten', |
927 | 933 |
'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)', |
928 | 934 |
'Edit project' => 'Projekt bearbeiten', |
... | ... | |
937 | 943 |
'Edit templates' => 'Vorlagen bearbeiten', |
938 | 944 |
'Edit text block' => 'Textblock bearbeiten', |
939 | 945 |
'Edit text block \'#1\'' => 'Textblock \'#1\' bearbeiten', |
946 |
'Edit text block picture #1' => 'Textblockbild #1 bearbeiten', |
|
940 | 947 |
'Edit the Delivery Order' => 'Lieferschein bearbeiten', |
941 | 948 |
'Edit the configuration for periodic invoices' => 'Konfiguration für wiederkehrende Rechnungen bearbeiten', |
942 | 949 |
'Edit the currency names in order to rename them.' => 'Bearbeiten Sie den Namen, um eine Währung umzubennen.', |
... | ... | |
1372 | 1379 |
'Long Description' => 'Langtext', |
1373 | 1380 |
'MAILED' => 'Gesendet', |
1374 | 1381 |
'MD' => 'PT', |
1382 |
'MIME type' => 'MIME-Typ', |
|
1375 | 1383 |
'Machine' => 'Maschine', |
1376 | 1384 |
'Main Preferences' => 'Grundeinstellungen', |
1377 | 1385 |
'Main sorting' => 'Hauptsortierung', |
... | ... | |
1495 | 1503 |
'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.', |
1496 | 1504 |
'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.', |
1497 | 1505 |
'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.', |
1506 |
'No picture has been uploaded' => 'Es wurde kein Bild hochgeladen', |
|
1507 |
'No picture uploaded yet' => 'Noch kein Bild hochgeladen', |
|
1498 | 1508 |
'No predefined texts has been created yet.' => 'Es wurden noch keine vordefinierten Textblöcken angelegt.', |
1499 | 1509 |
'No prices will be updated because no prices have been entered.' => 'Es werden keine Preise aktualisiert, weil keine gültigen Preisänderungen eingegeben wurden.', |
1500 | 1510 |
'No print templates have been created for this client yet. Please do so in the client configuration.' => 'Für diesen Mandanten wurden noch keine Druckvorlagen angelegt. Bitte holen Sie dies in der Mandantenkonfiguration nach.', |
... | ... | |
1685 | 1695 |
'Phone1' => 'Telefon 1 ', |
1686 | 1696 |
'Phone2' => 'Telefon 2', |
1687 | 1697 |
'Pick List' => 'Sammelliste', |
1698 |
'Picture #1: #2' => 'Abbildung #1: #2', |
|
1688 | 1699 |
'Pictures for parts' => 'Bilder für Waren', |
1689 | 1700 |
'Pictures for search parts' => 'Bilder für Warensuche', |
1690 | 1701 |
'Please Check the bank information for each customer:' => 'Bitte überprüfen Sie die Bankinformationen der Kunden:', |
... | ... | |
2017 | 2028 |
'Select a vendor' => 'Einen Lieferanten auswählen', |
2018 | 2029 |
'Select all' => 'Alle auswählen', |
2019 | 2030 |
'Select federal state...' => 'Bundesland auswählen...', |
2031 |
'Select file to upload' => 'Datei zum Hochladen auswählen', |
|
2020 | 2032 |
'Select from one of the items below' => 'Wählen Sie einen der untenstehenden Einträge', |
2021 | 2033 |
'Select from one of the names below' => 'Wählen Sie einen der untenstehenden Namen', |
2022 | 2034 |
'Select from one of the projects below' => 'Wählen Sie eines der untenstehenden Projekte', |
... | ... | |
2236 | 2248 |
'Test and preview' => 'Test und Vorschau', |
2237 | 2249 |
'Test database connectivity' => 'Datenbankverbindung testen', |
2238 | 2250 |
'Text block actions' => 'Textblockaktionen', |
2251 |
'Text block picture actions' => 'Aktionen für Textblockbilder', |
|
2239 | 2252 |
'Text blocks back' => 'Textblöcke hinten', |
2240 | 2253 |
'Text blocks front' => 'Textblöcke vorne', |
2241 | 2254 |
'Text field' => 'Textfeld', |
... | ... | |
2347 | 2360 |
'The existing record has been created from the link target to add.' => 'Der bestehende Beleg wurde aus dem auszuwählenden Verknüpfungsziel erstellt.', |
2348 | 2361 |
'The factor is missing in row %d.' => 'Der Faktor fehlt in Zeile %d.', |
2349 | 2362 |
'The factor is missing.' => 'Der Faktor fehlt.', |
2363 |
'The file name is missing' => 'Der Dateiname fehlt', |
|
2350 | 2364 |
'The first reason is that kivitendo contained a bug which resulted in the wrong taxkeys being recorded for transactions in which two entries are posted for the same chart with different taxkeys.' => 'Der erste Grund war ein Fehler in kivitendo, der dazu führte, dass bei einer Transaktion, bei der zwei Buchungen mit unterschiedlichen Steuerschlüsseln auf dasselbe Konto durchgeführt wurden, die falschen Steuerschlüssel gespeichert wurden.', |
2351 | 2365 |
'The follow-up date is missing.' => 'Das Wiedervorlagedatum fehlt.', |
2352 | 2366 |
'The following currencies have been used, but they are not defined:' => 'Die folgenden Währungen wurden benutzt, sind aber nicht ordnungsgemäß in der Datenbank eingetragen:', |
... | ... | |
2634 | 2648 |
'Unknown dependency \'%s\'.' => 'Unbekannte Abhängigkeit \'%s\'.', |
2635 | 2649 |
'Unknown problem type.' => 'Unbekannter Problem-Typ', |
2636 | 2650 |
'Unlock System' => 'System entsperren', |
2651 |
'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)', |
|
2637 | 2652 |
'Until' => 'Bis', |
2638 | 2653 |
'Update' => 'Erneuern', |
2639 | 2654 |
'Update Prices' => 'Preise aktualisieren', |
... | ... | |
2649 | 2664 |
'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren', |
2650 | 2665 |
'Updating prices of existing entry in database' => 'Preis des Eintrags in der Datenbank wird aktualisiert', |
2651 | 2666 |
'Updating the client fields in the database "#1" on host "#2:#3" failed.' => 'Die Aktualisierung der Mandantenfelder in der Datenbank "#1" auf Host "#2:#3" schlug fehl.', |
2667 |
'Uploaded at' => 'Hochgeladen um', |
|
2652 | 2668 |
'Uploaded on #1, size #2 kB' => 'Am #1 hochgeladen, Größe #2 kB', |
2653 | 2669 |
'Use As New' => 'Als neu verwenden', |
2654 | 2670 |
'Use WebDAV Repository' => 'WebDAV-Ablage verwenden', |
... | ... | |
2690 | 2706 |
'Vendor missing!' => 'Lieferant fehlt!', |
2691 | 2707 |
'Vendor not on file or locked!' => 'Dieser Lieferant existiert nicht oder ist gesperrt.', |
2692 | 2708 |
'Vendor not on file!' => 'Lieferant ist nicht in der Datenbank!', |
2693 |
'Vendor saved!' => 'Lieferant gespeichert!', |
|
2694 | 2709 |
'Vendor saved' => 'Lieferant gespeichert', |
2710 |
'Vendor saved!' => 'Lieferant gespeichert!', |
|
2695 | 2711 |
'Vendor type' => 'Lieferantentyp', |
2696 | 2712 |
'Vendors' => 'Lieferanten', |
2697 | 2713 |
'Verrechnungseinheit' => 'Verrechnungseinheit', |
2714 |
'Version' => 'Version', |
|
2698 | 2715 |
'Version actions' => 'Aktionen für Versionen', |
2699 | 2716 |
'Version number' => 'Versionsnummer', |
2700 |
'Version' => 'Version', |
|
2701 | 2717 |
'Versions' => 'Versionen', |
2702 | 2718 |
'View SEPA export' => 'SEPA-Export-Details ansehen', |
2703 | 2719 |
'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen', |
sql/Pg-upgrade2/requirement_spec_pictures.sql | ||
---|---|---|
1 |
-- @tag: requirement_spec_pictures |
|
2 |
-- @description: Pflichtenhefte: Support für Bilder |
|
3 |
-- @depends: requirement_specs |
|
4 |
|
|
5 |
CREATE TABLE requirement_spec_pictures ( |
|
6 |
id SERIAL NOT NULL, |
|
7 |
requirement_spec_id INTEGER NOT NULL, |
|
8 |
text_block_id INTEGER NOT NULL, |
|
9 |
position INTEGER NOT NULL, |
|
10 |
number TEXT NOT NULL, |
|
11 |
description TEXT, |
|
12 |
picture_file_name TEXT NOT NULL, |
|
13 |
picture_content_type TEXT NOT NULL, |
|
14 |
picture_mtime TIMESTAMP NOT NULL DEFAULT now(), |
|
15 |
picture_content BYTEA NOT NULL, |
|
16 |
picture_width INTEGER NOT NULL, |
|
17 |
picture_height INTEGER NOT NULL, |
|
18 |
thumbnail_content_type TEXT NOT NULL, |
|
19 |
thumbnail_content BYTEA NOT NULL, |
|
20 |
thumbnail_width INTEGER NOT NULL, |
|
21 |
thumbnail_height INTEGER NOT NULL, |
|
22 |
itime TIMESTAMP NOT NULL DEFAULT now(), |
|
23 |
mtime TIMESTAMP, |
|
24 |
|
|
25 |
PRIMARY KEY (id), |
|
26 |
FOREIGN KEY (requirement_spec_id) REFERENCES requirement_specs (id) ON DELETE CASCADE, |
|
27 |
FOREIGN KEY (text_block_id) REFERENCES requirement_spec_text_blocks (id) ON DELETE CASCADE |
|
28 |
); |
|
29 |
|
|
30 |
CREATE TRIGGER mtime_requirement_spec_pictures BEFORE UPDATE ON requirement_spec_pictures FOR EACH ROW EXECUTE PROCEDURE set_mtime(); |
|
31 |
|
|
32 |
ALTER TABLE requirement_specs ADD COLUMN previous_picture_number INTEGER; |
|
33 |
UPDATE requirement_specs SET previous_picture_number = 0; |
|
34 |
ALTER TABLE requirement_specs ALTER COLUMN previous_picture_number SET NOT NULL; |
|
35 |
ALTER TABLE requirement_specs ALTER COLUMN previous_picture_number SET DEFAULT 0; |
templates/print/Standard/requirement_spec.tex | ||
---|---|---|
85 | 85 |
\end{longtable} |
86 | 86 |
%$( END )$ |
87 | 87 |
|
88 |
%$( BLOCK picture_outputter )$ |
|
89 |
% $( SET width_cm = (picture.picture_width / 150.0) * 2.54 )$ |
|
90 |
% $( SET width_cm = width_cm < 16.4 ? width_cm : 16.4 )$ |
|
91 |
\begin{figure}[h!] |
|
92 |
\centering |
|
93 |
\includegraphics[width=$( width_cm )$cm,keepaspectratio]{$( picture.print_file_name )$} |
|
94 |
|
|
95 |
\mbox{Abbildung $( picture.number )$: $( LxLatex.filter(picture.description ? picture.description : picture.picture_file_name) )$} |
|
96 |
\end{figure} |
|
97 |
%$( END )$ |
|
98 |
|
|
88 | 99 |
%$( BLOCK text_block_outputter )$ |
89 | 100 |
% $( SET text_blocks = rspec.text_blocks_sorted(output_position=output_position) )$ |
90 | 101 |
% $( IF text_blocks.size )$ |
... | ... | |
99 | 110 |
|
100 | 111 |
$( LxLatex.filter(text_block.text) )$ |
101 | 112 |
|
113 |
% $( FOREACH picture = text_block.pictures_sorted.as_list )$ |
|
114 |
$( PROCESS picture_outputter picture=picture )$ |
|
115 |
% $( END )$ |
|
116 |
|
|
102 | 117 |
% $( END )$ |
103 | 118 |
% $( END )$ |
104 | 119 |
%$( END )$ |
templates/webpages/requirement_spec_text_block/_picture_form.html | ||
---|---|---|
1 |
[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE JavaScript -%][% USE Base64 %][% SET style="width: 500px" %] |
|
2 |
[% SET id_base = 'edit_text_block_picture_' _ (SELF.picture.id ? SELF.picture.id : 'new') %] |
|
3 |
<form method="post" id="[% id_base %]_form" method="POST" enctype="multipart/form-data"> |
|
4 |
[% L.hidden_tag('form_prefix', id_base, id=id_base _ '_form_prefix') %] |
|
5 |
[% L.hidden_tag('id', SELF.picture.id, no_id=1) %] |
|
6 |
[% L.hidden_tag(id_base _ '.text_block_id', SELF.text_block.id) %] |
|
7 |
[% L.hidden_tag(id_base _ '.requirement_spec_id', SELF.text_block.requirement_spec_id) %] |
|
8 |
|
|
9 |
<h2> |
|
10 |
[%- IF SELF.picture.id %] |
|
11 |
[%- LxERP.t8("Edit text block picture #1", SELF.picture.number) %] |
|
12 |
[%- ELSE %] |
|
13 |
[%- LxERP.t8("Add picture to text block") %] |
|
14 |
[%- END %] |
|
15 |
</h2> |
|
16 |
|
|
17 |
<table> |
|
18 |
[% IF SELF.picture.number %] |
|
19 |
<tr> |
|
20 |
<th align="right">[%- LxERP.t8("Number") %]:</th> |
|
21 |
<td>[% HTML.escape(SELF.picture.number) %]</td> |
|
22 |
</tr> |
|
23 |
[% END %] |
|
24 |
|
|
25 |
<tr> |
|
26 |
<th align="right">[%- LxERP.t8("Description") %]:</th> |
|
27 |
<td>[% L.input_tag(id_base _ '.description', SELF.picture.description, style=style) %]</td> |
|
28 |
</tr> |
|
29 |
|
|
30 |
[% IF SELF.picture.picture_content %] |
|
31 |
<tr> |
|
32 |
<th align="right">[%- LxERP.t8("File name") %]:</th> |
|
33 |
<td>[% HTML.escape(SELF.picture.picture_file_name) %]</td> |
|
34 |
</tr> |
|
35 |
|
|
36 |
<tr> |
|
37 |
<th align="right">[%- LxERP.t8("MIME type") %]:</th> |
|
38 |
<td>[% HTML.escape(SELF.picture.picture_content_type) %]</td> |
|
39 |
</tr> |
|
40 |
|
|
41 |
<tr> |
|
42 |
<th align="right">[%- LxERP.t8("Dimensions") %]:</th> |
|
43 |
<td>[% HTML.escape(SELF.picture.picture_width) %]x[% HTML.escape(SELF.picture.picture_height) %]</td> |
|
44 |
</tr> |
|
45 |
|
|
46 |
<tr> |
|
47 |
<th align="right">[%- LxERP.t8("Uploaded at") %]:</th> |
|
48 |
<td>[% HTML.escape(SELF.picture.picture_mtime.to_kivitendo(precision='second')) %]</td> |
|
49 |
</tr> |
|
50 |
[% END %] |
|
51 |
|
|
52 |
<tr> |
|
53 |
<th align="right">[%- LxERP.t8("Select file to upload") %]:</th> |
|
54 |
<td>[% L.input_tag(id_base _ '.picture_content', '', type='file') %]</td> |
|
55 |
</tr> |
|
56 |
</table> |
|
57 |
|
|
58 |
<p> |
|
59 |
[%- L.ajax_submit_tag('controller.pl?action=RequirementSpecTextBlock/ajax_' _ (SELF.picture.id ? 'update' : 'create') _ '_picture', '#' _ id_base _ '_form', LxERP.t8('Save'), no_id=1) %] |
|
60 |
<a href="#" onclick="$('#jqueryui_popup_dialog').dialog('close');">[%- LxERP.t8("Cancel") %]</a> |
|
61 |
</p> |
|
62 |
|
|
63 |
</form> |
|
64 |
|
|
65 |
[% IF SELF.picture.id %] |
|
66 |
<h2>[% LxERP.t8("Current picture") %]</h2> |
|
67 |
|
|
68 |
<div> |
|
69 |
<img src="data:[% HTML.escape(SELF.picture.picture_content_type) %];base64,[% SELF.picture.picture_content.encode_base64 %]"> |
|
70 |
</div> |
|
71 |
[% END %] |
templates/webpages/requirement_spec_text_block/_text_block.html | ||
---|---|---|
11 | 11 |
[% L.simple_format(text_block.text) %] |
12 | 12 |
</div> |
13 | 13 |
[% END %] |
14 |
|
|
15 |
<div class="clearfix" id="text-block-[% HTML.escape(text_block.id) %]-pictures"[% UNLESS text_block.pictures.as_list.size %] style="display: none"[% END %]> |
|
16 |
[% FOREACH picture = text_block.pictures_sorted %] |
|
17 |
[% INCLUDE 'requirement_spec_text_block/_text_block_picture.html' picture=picture %] |
|
18 |
[% END %] |
|
19 |
</div> |
|
14 | 20 |
</div> |
templates/webpages/requirement_spec_text_block/_text_block_picture.html | ||
---|---|---|
1 |
[%- USE HTML -%][%- USE LxERP -%][% USE Base64 %] |
|
2 |
[% SET title=LxERP.t8('Picture #1: #2', picture.number, picture.description ? picture.description _ ' (' _ picture.picture_file_name _ ')' : picture.picture_file_name) %] |
|
3 |
<div id="text-block-picture-[% HTML.escape(picture.id) %]" class="requirement-spec-text-block-picture-thumbnail text-block-picture-context-menu"> |
|
4 |
<div class="requirement-spec-text-block-picture-thumbnail-img-container"> |
|
5 |
<img src="data:[% HTML.escape(picture.thumbnail_content_type) %];base64,[% picture.thumbnail_content.encode_base64 %]" alt="[% title %]"> |
|
6 |
</div> |
|
7 |
<div> |
|
8 |
[% LxERP.t8('Picture #1: #2', picture.number, picture.description ? picture.description : picture.picture_file_name) %] |
|
9 |
</div> |
|
10 |
</div> |
Auch abrufbar als: Unified diff
Pflichtenhefte: Unterstützung für an Textblöcke angehängte Bilder