1 package SL::Controller::File;
5 use parent qw(SL::Controller::Base);
7 use List::Util qw(first max);
10 use Encode qw(decode);
16 use File::Spec::Win32;
17 use File::MimeInfo::Magic;
18 use SL::DB::Helper::Mappings;
20 use SL::DB::DeliveryOrder;
23 use SL::DB::PurchaseInvoice;
25 use SL::DB::GLTransaction;
29 use SL::Helper::CreatePDF qw(:all);
30 use SL::Locale::String;
33 use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type);
35 use constant DO_DELETE => 0;
36 use constant DO_UNIMPORT => 1;
38 use Rose::Object::MakeMethods::Generic
40 'scalar --get_set_init' => [ qw() ],
41 'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
44 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
47 'sales_quotation' => { gen => 1, gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
48 'sales_order' => { gen => 1, gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
49 'sales_delivery_order' => { gen => 1, gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
50 'invoice' => { gen => 1, gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
51 'credit_note' => { gen => 1, gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
52 'request_quotation' => { gen => 3, gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
53 'purchase_order' => { gen => 3, gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
54 'purchase_delivery_order' => { gen => 3, gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
55 'purchase_invoice' => { gen => 2, gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
56 'vendor' => { gen => 0, gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
57 'customer' => { gen => 1, gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
58 'part' => { gen => 0, gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
59 'gl_transaction' => { gen => 2, gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
60 'draft' => { gen => 0, gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
61 'csv_customer' => { gen => 1, gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
62 'csv_vendor' => { gen => 1, gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
63 'shop_image' => { gen => 0, gltype => '', dir =>'ShopImages', model => 'Part', right => 'xx' },
67 # $main::locale->text('imported')
77 $is_json = 1 if $::form->{json};
79 $self->_do_list($is_json);
82 sub action_ajax_importdialog {
84 $::auth->assert($self->object_right);
85 my $path = $::form->{path};
86 my @files = $self->_get_from_import($path);
88 'name' => $::form->{source},
90 'chk_action' => $::form->{source}.'_import',
91 'chk_title' => $main::locale->text('Import scanned documents'),
92 'chkall_title' => $main::locale->text('Import all'),
95 $self->render('file/import_dialog',
102 sub action_ajax_import {
104 $::auth->assert($self->object_right);
105 my $ids = $::form->{ids};
106 my $source = $::form->{source};
107 my $path = $::form->{path};
108 my @files = $self->_get_from_import($path);
109 foreach my $filename (@{ $::form->{$ids} || [] }) {
110 my ($file, undef) = grep { $_->{name} eq $filename } @files;
112 my $obj = SL::File->save(object_id => $self->object_id,
113 object_type => $self->object_type,
114 mime_type => 'application/pdf',
116 file_type => 'document',
117 file_name => $file->{filename},
118 file_path => $file->{path}
120 unlink($file->{path}) if $obj;
126 sub action_ajax_delete {
128 $self->_delete_all(DO_DELETE, $::locale->text('Following files are deleted:'));
131 sub action_ajax_unimport {
133 $self->_delete_all(DO_UNIMPORT, $::locale->text('Following files are unimported:'));
136 sub action_ajax_rename {
138 my ($id, $version) = split /_/, $::form->{id};
139 my $file = SL::File->get(id => $id);
141 $self->js->flash('error', $::locale->text('File not exists !'))->render();
144 my $sessionfile = $::form->{sessionfile};
145 if ( $sessionfile && -f $sessionfile ) {
147 if ( $::form->{to} eq $file->file_name ) {
148 # no rename so use as new version
149 $file->save_file($sessionfile);
150 $self->js->flash('warning', $::locale->text('File \'#1\' is used as new Version !', $file->file_name));
153 # new filename, so it is a new file with the same attributes as the old file
155 SL::File->save(object_id => $file->object_id,
156 object_type => $file->object_type,
157 mime_type => $file->mime_type,
158 source => $file->source,
159 file_type => $file->file_type,
160 file_name => $::form->{to},
161 file_path => $sessionfile
163 unlink($sessionfile);
166 $self->js->flash( 'error', t8('internal error (see details)'))
167 ->flash_detail('error', $@)->render;
177 $result = $file->rename($::form->{to});
180 $self->js->flash( 'error', t8('internal error (see details)'))
181 ->flash_detail('error', $@)->render;
185 if ($result != SL::File::RENAME_OK) {
186 $self->js->flash('error',
187 $result == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
188 : $result == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
189 : $::locale->text('File not exists !'))
194 $self->is_global($::form->{is_global});
195 $self->file_type( $file->file_type);
196 $self->object_type($file->object_type);
197 $self->object_id( $file->object_id);
198 #$self->object_model($file_types{$file->module}->{model});
199 #$self->object_right($file_types{$file->module}->{right});
200 if ( $::form->{next_ids} ) {
201 my @existing = split(/,/, $::form->{next_ids});
202 $self->existing(\@existing);
207 sub action_ajax_upload {
209 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
210 $self->{accept_types} = '';
211 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
212 $self->render('file/upload_dialog',
218 sub action_ajax_files_uploaded {
221 my $source = 'uploaded';
223 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
224 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
225 foreach my $idx (0 .. scalar(@upfiles) - 1) {
227 my $fname = uri_unescape($upfiles[$idx]->{filename});
228 # normalize and find basename
229 # first split with unix rules
230 # after that split with windows rules
231 my ($volume, $directories, $basefile) = File::Spec::Unix->splitpath($fname);
232 ($volume, $directories, $basefile) = File::Spec::Win32->splitpath($basefile);
234 # to find real mime_type by magic we must save the filedata
236 my $sess_fname = "file_upload_" . $self->object_type . "_" . $self->object_id . "_" . $idx;
237 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
239 $sfile->fh->print(${$upfiles[$idx]->{data}});
241 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
244 # if filename has the suffix "pdf", but isn't really a pdf, set mimetype for no suffix
245 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
246 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
248 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
251 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
252 object_type => $self->object_type,
253 mime_type => $mime_type,
255 file_type => $self->file_type,
256 file_name => $basefile,
260 push @existing, $existobj->id.'_'.$sfile->file_name;
262 my $fileobj = SL::File->save(object_id => $self->object_id,
263 object_type => $self->object_type,
264 mime_type => $mime_type,
266 file_type => $self->file_type,
267 file_name => $basefile,
268 title => $::form->{title},
269 description => $::form->{description},
270 ## two possibilities: what is better ? content or sessionfile ??
271 file_contents => ${$upfiles[$idx]->{data}},
272 file_path => $sfile->file_name
274 unlink($sfile->file_name);
278 $self->js->flash( 'error', t8('internal error (see details)'))
279 ->flash_detail('error', $@)->render;
284 $self->existing(\@existing);
288 sub action_download {
290 my ($id, $version) = split /_/, $::form->{id};
291 my $file = SL::File->get(id => $id );
292 $file->version($version) if $version;
293 my $ref = $file->get_content;
294 if ( $file && $ref ) {
295 return $self->send_file($ref,
296 type => $file->mime_type,
297 name => $file->file_name,
306 sub check_object_params {
309 my $id = ($::form->{object_id} // 0) * 1;
310 my $draftid = ($::form->{draft_id} // 0) * 1;
314 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
316 $type = $::form->{object_type};
319 $id = $::form->{draft_id};
321 } elsif ( $::form->{object_type} ) {
322 $type = $::form->{object_type};
324 die "No object type" unless $type;
325 die "No file type" unless $::form->{file_type};
326 die "Unkown object type" unless $file_types{$type};
328 $self->is_global($gldoc);
329 $self->file_type($::form->{file_type});
330 $self->object_type($type);
331 $self->object_id($id);
332 $self->object_model($file_types{$type}->{model});
333 $self->object_right($file_types{$type}->{right});
335 # $::auth->assert($self->object_right);
337 # my $model = 'SL::DB::' . $self->object_model;
338 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
348 my ($self, $do_unimport, $infotext) = @_;
350 my $ids = $::form->{ids};
351 foreach my $id_version (@{ $::form->{$ids} || [] }) {
352 my ($id, $version) = split /_/, $id_version;
353 my $dbfile = SL::File->get(id => $id);
356 $dbfile->version($version);
357 $files .= ' ' . $dbfile->file_name if $dbfile->delete_version;
359 $files .= ' ' . $dbfile->file_name if $dbfile->delete;
363 $self->js->flash('info', $infotext . $files) if $files;
368 my ($self, $json) = @_;
370 if ( $self->file_type eq 'document' ) {
372 push @object_types, $self->object_type;
373 push @object_types, qw(dunning dunning1 dunning2 dunning3) if $self->object_type eq 'invoice'; # hardcoded object types?
374 @files = SL::File->get_all_versions(object_id => $self->object_id,
375 object_type => \@object_types,
376 file_type => $self->file_type,
380 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
381 @files = SL::File->get_all(object_id => $self->object_id,
382 object_type => $self->object_type,
383 file_type => $self->file_type,
386 $self->files(\@files);
388 if($self->object_type eq 'shop_image'){
390 ->run('kivi.ShopPart.show_images', $self->object_id)
393 $self->_mk_render('file/list', 1, 0, $json);
397 sub _get_from_import {
398 my ($self, $path) = @_;
401 my $language = $::lx_office_conf{system}->{language};
402 my $timezone = $::locale->get_local_time_zone()->name;
403 if (opendir my $dir, $path) {
404 my @files = ( readdir $dir);
405 foreach my $file ( @files) {
406 next if (($file eq '.') || ($file eq '..'));
407 $file = Encode::decode('utf-8', $file);
409 next if ( -d "$path/$file" );
411 my $tmppath = File::Spec->catfile( $path, $file );
412 next if( ! -f $tmppath );
414 my $st = stat($tmppath);
415 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language );
416 my $sname = $main::locale->quote_special_chars('HTML', $file);
419 'filename' => $sname,
421 'mtime' => $st->mtime,
422 'date' => $dt->dmy('.') . " " . $dt->hms,
431 my ($self, $template, $edit, $scanner, $json) = @_;
434 ##TODO make code configurable
437 my @sources = $self->_get_sources();
438 foreach my $source ( @sources ) {
439 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
441 if ( $self->file_type eq 'document' ) {
442 $title = $main::locale->text('Documents');
443 } elsif ( $self->file_type eq 'attachment' ) {
444 $title = $main::locale->text('Attachments');
445 } elsif ( $self->file_type eq 'image' ) {
446 $title = $main::locale->text('Images');
449 my $output = SL::Presenter->get->render(
452 SOURCES => \@sources,
453 edit_attachments => $edit,
454 object_type => $self->object_type,
455 object_id => $self->object_id,
456 file_type => $self->file_type,
457 is_global => $self->is_global,
461 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
462 if ( $self->existing && scalar(@{$self->existing}) > 0) {
463 my $first = shift @{$self->existing};
464 my ($first_id, $sfile) = split('_', $first, 2);
465 my $file = SL::File->get(id => $first_id );
466 $self->js->run('kivi.File.askForRename', $first_id, $file->file_type, $file->file_name, $sfile, join (',', @{$self->existing}), $self->is_global);
470 $self->render(\$output, { layout => 0, process => 0 });
475 $self->js->flash( 'error', t8('internal error (see details)'))
476 ->flash_detail('error', $@)->render;
478 $self->render('generic/error', { layout => 0 }, label_error => $@);
487 if ( $self->file_type eq 'document' ) {
488 # TODO statt gen neue attribute in filetypes :
489 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
492 'title' => $main::locale->text('generated Files'),
493 'chk_action' => 'documents_delete',
494 'chk_title' => $main::locale->text('Delete Documents'),
495 'chkall_title' => $main::locale->text('Delete all'),
496 'file_title' => $main::locale->text('filename'),
497 'confirm_text' => $main::locale->text('delete'),
499 'rename_title' => $main::locale->text('Rename Documents'),
500 'done_text' => $main::locale->text('deleted')
502 push @sources , $gendata;
504 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
505 my @others = SL::File->get_other_sources();
506 foreach my $scanner_or_mailrx (@others) {
508 'name' => $scanner_or_mailrx->{name},
509 'title' => $main::locale->text('from \'#1\' imported Files', $scanner_or_mailrx->{description}),
510 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
511 'chk_title' => $main::locale->text('Unimport documents'),
512 'chkall_title' => $main::locale->text('Unimport all'),
513 'file_title' => $main::locale->text('filename'),
514 'confirm_text' => $main::locale->text('unimport'),
516 'rename_title' => $main::locale->text('Rename Documents'),
518 'import_title' => $main::locale->text('Add Document from \'#1\'', $scanner_or_mailrx->{name}),
519 'path' => $scanner_or_mailrx->{directory},
520 'done_text' => $main::locale->text('unimported')
522 push @sources , $other;
526 elsif ( $self->file_type eq 'attachment' ) {
528 'name' => 'uploaded',
529 'title' => $main::locale->text(''),
530 'chk_action' => 'attachments_delete',
531 'chk_title' => $main::locale->text('Delete Attachments'),
532 'chkall_title' => $main::locale->text('Delete all'),
533 'file_title' => $main::locale->text('filename'),
534 'confirm_text' => $main::locale->text('delete'),
536 'are_existing' => $self->existing ? 1 : 0,
537 'rename_title' => $main::locale->text('Rename Attachments'),
539 'upload_title' => $main::locale->text('Upload Attachments'),
540 'done_text' => $main::locale->text('deleted')
542 push @sources , $attdata;
544 elsif ( $self->file_type eq 'image' ) {
546 'name' => 'uploaded',
547 'title' => $main::locale->text(''),
548 'chk_action' => 'images_delete',
549 'chk_title' => $main::locale->text('Delete Images'),
550 'chkall_title' => $main::locale->text('Delete all'),
551 'file_title' => $main::locale->text('filename'),
552 'confirm_text' => $main::locale->text('delete'),
554 'are_existing' => $self->existing ? 1 : 0,
555 'rename_title' => $main::locale->text('Rename Images'),
557 'upload_title' => $main::locale->text('Upload Images'),
558 'done_text' => $main::locale->text('deleted')
560 push @sources , $attdata;
575 SL::Controller::File - Controller for managing files
579 The Controller is called directly from the webpages
581 <a href="controller.pl?action=File/list&file_type=document\
582 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
585 or indirectly via javascript functions from js/kivi.File.js
587 kivi.popup_dialog({ url: 'controller.pl',
588 data: { action : 'File/ajax_upload',
589 file_type : 'uploaded',
597 This is a controller for handling files in a storage independent way.
598 The storage may be a Filesystem,a WebDAV, a Database or DMS.
599 These backends must be configered in ClientConfig.
600 This Controller use as intermediate layer for storage C<SL::File>.
602 The Controller is responsible to display forms for displaying the files at the ERP-objects and
603 for uploading and downloading the files.
605 More description of the intermediate layer see L<SL::File>.
609 =head2 C<action_list>
611 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
612 Dependent of file_type different sources are available.
614 For documents there are the 'created' source and the imports from scanners or email.
615 For attachments and images only the 'uploaded' source available.
617 Available C<FORM PARAMS>:
621 =item C<form.object_id>
623 The Id of the ERP-object.
625 =item C<form.object_type>
627 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
629 =item C<form.file_type>
631 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
632 This file_type is a filter for the list.
636 The method can be used as normal HTTP-Request (json=0) or as AJAX-JSON call to refresh the list if the parameter is set to 1.
641 =head2 C<action_ajax_upload>
644 A new file or more files can selected by a dialog and insert into the system.
647 Available C<FORM PARAMS>:
651 =item C<form.file_type>
653 This parameter describe here the source for a new file :
654 "attachments" and "images"
656 This is a normal upload selection, which may be more then one file to upload.
658 =item C<form.object_id>
662 =item C<form.object_type>
664 are the same as at C<action_list>
668 =head2 C<action_ajax_files_uploaded>
670 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
671 The filepaths are checked about Unix and Windows paths. Also the MIME type of the files are verified ( IS the contents of a *.pdf real PDF?).
672 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
674 If the filename is not changed the new uploaded file is a new version of the file, if the name is changed it is a new file.
676 Available C<FORM PARAMS>:
680 =item C<form.ATTACHMENTS.uploadfiles>
682 This is an array of elements which have {filename} for the name and {data} for the contents.
684 Also object_id, object_type and file_type
688 =head2 C<action_download>
690 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
692 Available C<FORM PARAMS>:
696 Also object_id, object_type and file_type
700 =head2 C<action_ajax_importdialog>
702 A Dialog with all available and not imported files to import is open.
703 More then one file can be selected.
705 Available C<FORM PARAMS>:
711 The name of the source like "scanner1" or "email"
715 The full path to the directory on the server, where the files to import can found
717 Also object_id, object_type and file_type
721 =head2 C<action_ajax_delete>
723 Some files can be deleted
725 Available C<FORM PARAMS>:
731 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
735 =head2 C<action_ajax_unimport>
737 Some files can be unimported, dependent of the source of the file. This means they are moved
738 back to the directory of the source
740 Available C<FORM PARAMS>:
746 The ids of the files to unimport. Only these files are unimported not all versions of a file if the exists
750 =head2 C<action_ajax_rename>
752 One file can be renamed. There can be some checks if the same filename still exists at one object.
756 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>