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;
39 use Rose::Object::MakeMethods::Generic
41 'scalar --get_set_init' => [ qw() ],
42 'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ],
45 __PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]);
48 'sales_quotation' => { gen => 1 ,gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' },
49 'sales_order' => { gen => 1 ,gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' },
50 'sales_delivery_order' => { gen => 1 ,gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' },
51 'invoice' => { gen => 1 ,gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' },
52 'credit_note' => { gen => 1 ,gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' },
53 'request_quotation' => { gen => 3 ,gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' },
54 'purchase_order' => { gen => 3 ,gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' },
55 'purchase_delivery_order' => { gen => 3 ,gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' },
56 'purchase_invoice' => { gen => 2 ,gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' },
57 'vendor' => { gen => 0 ,gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' },
58 'customer' => { gen => 1 ,gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' },
59 'part' => { gen => 0 ,gltype => '', dir =>'Part', model => 'Part', right => 'xx' },
60 'gl_transaction' => { gen => 2 ,gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' },
61 'draft' => { gen => 0 ,gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' },
62 'csv_customer' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' },
63 'csv_vendor' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' },
67 # $main::locale->text('imported')
77 $isjson = 1 if $::form->{json};
79 $self->_do_list($isjson);
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 $file = SL::File->get(id => $::form->{id});
140 $self->js->flash('error',$::locale->text('File not exists !'))->render();
143 $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$file->object_id." object_type=".$file->object_type." dbfile=".$file);
144 my $sessionfile = $::form->{sessionfile};
145 $main::lxdebug->message(LXDebug->DEBUG2(), "sessionfile=".$sessionfile);
146 if ( $sessionfile && -f $sessionfile ) {
147 $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file->file_name." to=".$::form->{to}." sessionfile=".$sessionfile);
149 if ( $::form->{to} eq $file->file_name ) {
150 # no rename so use as new version
151 $file->save_file($sessionfile);
152 $self->js->flash('warning',$::locale->text('File \'#1\' is used as new Version !',$file->file_name));
155 # new filename so it is a new file with same attributes as old file
157 SL::File->save(object_id => $file->object_id,
158 object_type => $file->object_type,
159 mime_type => $file->mime_type,
160 source => $file->source,
161 file_type => $file->file_type,
162 file_name => $::form->{to},
163 file_path => $sessionfile
165 unlink($sessionfile);
168 $self->js->flash( 'error', t8('internal error (see details)'))
169 ->flash_detail('error', $@)->render;
179 $res = $file->rename($::form->{to});
180 $main::lxdebug->message(LXDebug->DEBUG2(), "rename result=".$res);
183 $self->js->flash( 'error', t8('internal error (see details)'))
184 ->flash_detail('error', $@)->render;
188 if ($res != SL::File::RENAME_OK) {
189 $self->js->flash('error',
190 $res == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !')
191 : $res == SL::File::RENAME_SAME ? $::locale->text('Same Filename !')
192 : $::locale->text('File not exists !'))
197 $self->is_global($::form->{is_global});
198 $self->file_type( $file->file_type);
199 $self->object_type($file->object_type);
200 $self->object_id( $file->object_id);
201 #$self->object_model($file_types{$file->module}->{model});
202 #$self->object_right($file_types{$file->module}->{right});
203 if ( $::form->{next_ids} ) {
204 my @existing = split(/,/, $::form->{next_ids});
205 $self->existing(\@existing);
210 sub action_ajax_upload {
212 $self->{maxsize} = $::instance_conf->get_doc_max_filesize;
213 $self->{accept_types} = '';
214 $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image';
215 $self->render('file/upload_dialog',
221 sub action_ajax_files_uploaded {
224 my $source = 'uploaded';
225 $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload UPLOAD=".$::form->{ATTACHMENTS}->{uploadfiles});
227 if ( $::form->{ATTACHMENTS}->{uploadfiles} ) {
228 my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} };
229 foreach my $idx (0 .. scalar(@upfiles) - 1) {
231 my $fname = uri_unescape($upfiles[$idx]->{filename});
232 $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload name=".$fname);
233 ## normalize and find basename
234 # first split with unix rules
235 # after that split with windows rules
236 my ($volume,$directories,$basefile) = File::Spec::Unix->splitpath($fname);
237 ($volume,$directories,$basefile) = File::Spec::Win32->splitpath($basefile);
239 # to find real mime_type by magic we must save the filedata
241 my $sess_fname = "file_upload_".$self->object_type."_".$self->object_id."_".$idx;
242 my $sfile = SL::SessionFile->new($sess_fname, mode => 'w');
244 $sfile->fh->print(${$upfiles[$idx]->{data}});
246 my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name);
249 # if filename has the suffix "pdf", but is really no pdf set mimetype for no suffix
250 $mime_type = File::MimeInfo::Magic::mimetype($basefile);
251 $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type;
253 $main::lxdebug->message(LXDebug->DEBUG2(), "mime_type=".$mime_type);
254 if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) {
257 my ($existobj) = SL::File->get_all(object_id => $self->object_id,
258 object_type => $self->object_type,
259 mime_type => $mime_type,
261 file_type => $self->file_type,
262 file_name => $basefile,
265 $main::lxdebug->message(LXDebug->DEBUG2(), "store1 exist=".$existobj);
267 $main::lxdebug->message(LXDebug->DEBUG2(), "id=".$existobj->id." sessionfile=". $sfile->file_name);
268 push @existing, $existobj->id.'_'.$sfile->file_name;
270 my $fileobj = SL::File->save(object_id => $self->object_id,
271 object_type => $self->object_type,
272 mime_type => $mime_type,
274 file_type => $self->file_type,
275 file_name => $basefile,
276 ## two possibilities: what is better ? content or sessionfile ??
277 #file_contents => ${$upfiles[$idx]->{data}},
278 file_path => $sfile->file_name
280 $main::lxdebug->message(LXDebug->DEBUG2(), "obj=".$fileobj);
281 unlink($sfile->file_name);
285 $self->js->flash( 'error', t8('internal error (see details)'))
286 ->flash_detail('error', $@)->render;
291 $self->existing(\@existing);
295 sub action_download {
297 my ($id,$version) = split /_/, $::form->{id};
298 my $file = SL::File->get(id => $id );
299 $file->version($version) if $version;
300 my $ref = $file->get_content;
301 if ( $file && $ref ) {
302 return $self->send_file($ref,
303 type => $file->mime_type,
304 name => $file->file_name,
313 sub check_object_params {
316 my $id = ($::form->{object_id} // 0) * 1;
317 my $draftid = ($::form->{draft_id} // 0) * 1;
321 if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) {
323 $type = $::form->{object_type};
326 $id = $::form->{draft_id};
328 } elsif ( $::form->{object_type} ) {
329 $type = $::form->{object_type};
331 die "No object type" if ! $type;
332 die "No file type" if ! $::form->{file_type};
333 die "Unkown object type" if ! $file_types{$type};
335 $self->is_global($gldoc);
336 $self->file_type($::form->{file_type});
337 $self->object_type($type);
338 $self->object_id($id);
339 $self->object_model($file_types{$type}->{model});
340 $self->object_right($file_types{$type}->{right});
341 $main::lxdebug->message(LXDebug->DEBUG2(), "checked: object_id=".$self->object_id." object_type=".$self->object_type." is_global=".$self->is_global);
343 # $::auth->assert($self->object_right);
345 # my $model = 'SL::DB::' . $self->object_model;
346 # $self->object($model->new(id => $self->object_id)->load || die "Record not found");
356 my ($self,$do_unimport,$infotext) = @_;
358 my $ids = $::form->{ids};
359 foreach my $id_version (@{ $::form->{$ids} || [] }) {
360 my ($id,$version) = split /_/, $id_version;
361 my $dbfile = SL::File->get(id => $id);
362 $dbfile->version($version) if $dbfile && $version;
363 if ( $dbfile && $dbfile->delete ) {
364 $files .= ' '.$dbfile->file_name;
367 $self->js->flash('info',$infotext.$files) if $files;
372 my ($self,$json) = @_;
374 $main::lxdebug->message(LXDebug->DEBUG2(), "do_list: object_id=".$self->object_id." object_type=".$self->object_type." file_type=".$self->file_type." json=".$json);
375 if ( $self->file_type eq 'document' ) {
377 push @object_types, $self->object_type;
378 push @object_types, ('dunning','dunning1','dunning2','dunning3') if $self->object_type eq 'invoice';
379 @files = SL::File->get_all_versions(object_id => $self->object_id ,
380 object_type => \@object_types,
381 file_type => $self->file_type );
383 $main::lxdebug->message(LXDebug->DEBUG2(), "cnt1=".scalar(@files));
385 elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) {
386 @files = SL::File->get_all(object_id => $self->object_id ,
387 object_type => $self->object_type,
388 file_type => $self->file_type );
389 $main::lxdebug->message(LXDebug->DEBUG2(), "cnt2=".scalar(@files));
391 $self->files(\@files);
392 $self->_mk_render('file/list',1,0,$json);
395 sub _get_from_import {
396 my ($self,$path) = @_;
399 $main::lxdebug->message(LXDebug->DEBUG2(), "import path=".$path);
400 my $language = $::lx_office_conf{system}->{language};
401 my $timezone = $::locale->get_local_time_zone()->name;
402 if (opendir my $dir, $path) {
403 my @files = ( readdir $dir);
404 foreach my $file ( @files) {
405 next if (($file eq '.') || ($file eq '..'));
406 $file = Encode::decode('utf-8', $file);
407 $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file);
409 next if( -d "$path/$file");
411 my $tmppath = File::Spec->catfile( $path, $file );
412 $main::lxdebug->message(LXDebug->DEBUG2(), "tmppath=".$tmppath." file=".$file);
413 next if( ! -f $tmppath);
415 my $st = stat($tmppath);
416 my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language);
417 my $sname = $main::locale->quote_special_chars('HTML',$file);
420 'filename' => $sname,
422 'mtime' => $st->mtime,
423 'date' => $dt->dmy('.')." ".$dt->hms,
428 $main::lxdebug->message(LXDebug->DEBUG2(), "return ".scalar(@foundfiles)." files");
433 my ($self,$template,$edit,$scanner,$json) = @_;
436 ##TODO here a configurable code must be implemented
439 $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: object_id=".$self->object_id." object_type=".$self->object_type.
440 " file_type=".$self->file_type." json=".$json." filecount=".scalar(@{ $self->files })." is_global=".$self->is_global);
441 my @sources = $self->_get_sources();
442 foreach my $source ( @sources ) {
443 $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: source name=".$source->{name});
444 @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files };
446 if ( $self->file_type eq 'document' ) {
447 $title = $main::locale->text('Documents');
448 } elsif ( $self->file_type eq 'attachment' ) {
449 $title = $main::locale->text('Attachments');
450 } elsif ( $self->file_type eq 'image' ) {
451 $title = $main::locale->text('Images');
454 my $output = SL::Presenter->get->render(
457 SOURCES => \@sources,
458 edit_attachments => $edit,
459 object_type => $self->object_type,
460 object_id => $self->object_id,
461 file_type => $self->file_type,
462 is_global => $self->is_global,
466 $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output);
467 if ( $self->existing && scalar(@{$self->existing}) > 0) {
468 my $first = shift @{$self->existing};
469 my ($first_id,$sfile) = split('_',$first,2);
470 #$main::lxdebug->message(LXDebug->DEBUG2(), "id=".$first_id." sessionfile=". $sfile);
471 my $file = SL::File->get(id => $first_id );
472 $self->js->run('kivi.File.askForRename',$first_id,$file->file_name,$sfile,join (',', @{$self->existing}), $self->is_global);
476 $self->render(\$output, { layout => 0, process => 0 });
481 $self->js->flash( 'error', t8('internal error (see details)'))
482 ->flash_detail('error', $@)->render;
484 $self->render('generic/error', { layout => 0 }, label_error => $@);
493 $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources file_type=". $self->file_type);
494 if ( $self->file_type eq 'document' ) {
495 ##TODO statt gen neue attribute in filetypes :
496 if (($file_types{$self->object_type}->{gen}*1 & 1)==1) {
499 'title' => $main::locale->text('generated Files'),
500 'chk_action' => 'documents_delete',
501 'chk_title' => $main::locale->text('Delete Documents'),
502 'chkall_title' => $main::locale->text('Delete all'),
503 'file_title' => $main::locale->text('filename'),
504 'confirm_text' => $main::locale->text('delete'),
506 'rename_title' => $main::locale->text('Rename Documents'),
507 'done_text' => $main::locale->text('deleted')
509 push @sources , $gendata;
511 if (($file_types{$self->object_type}->{gen}*1 & 2)==2) {
512 my @others = SL::File->get_other_sources();
513 $main::lxdebug->message(LXDebug->DEBUG2(), "other cnt=". scalar(@others));
514 foreach my $scanner_or_mailrx (@others) {
516 'name' => $scanner_or_mailrx->{name},
517 'title' => $main::locale->text('from \'#1\' imported Files',$scanner_or_mailrx->{description}),
518 'chk_action' => $scanner_or_mailrx->{name}.'_unimport',
519 'chk_title' => $main::locale->text('Unimport documents'),
520 'chkall_title' => $main::locale->text('Unimport all'),
521 'file_title' => $main::locale->text('filename'),
522 'confirm_text' => $main::locale->text('unimport'),
524 'rename_title' => $main::locale->text('Rename Documents'),
526 'import_title' => $main::locale->text('Add Document from \'#1\'',$scanner_or_mailrx->{name}),
527 'path' => $scanner_or_mailrx->{directory},
528 'done_text' => $main::locale->text('unimported')
530 push @sources , $other;
534 elsif ( $self->file_type eq 'attachment' ) {
536 'name' => 'uploaded',
537 'title' => $main::locale->text(''),
538 'chk_action' => 'attachments_delete',
539 'chk_title' => $main::locale->text('Delete Attachments'),
540 'chkall_title' => $main::locale->text('Delete all'),
541 'file_title' => $main::locale->text('filename'),
542 'confirm_text' => $main::locale->text('delete'),
544 'are_existing' => $self->existing ? 1 : 0,
545 'rename_title' => $main::locale->text('Rename Attachments'),
547 'upload_title' => $main::locale->text('Upload Attachments'),
548 'done_text' => $main::locale->text('deleted')
550 push @sources , $attdata;
552 elsif ( $self->file_type eq 'image' ) {
554 'name' => 'uploaded',
555 'title' => $main::locale->text(''),
556 'chk_action' => 'images_delete',
557 'chk_title' => $main::locale->text('Delete Images'),
558 'chkall_title' => $main::locale->text('Delete all'),
559 'file_title' => $main::locale->text('filename'),
560 'confirm_text' => $main::locale->text('delete'),
562 'are_existing' => $self->existing ? 1 : 0,
563 'rename_title' => $main::locale->text('Rename Images'),
565 'upload_title' => $main::locale->text('Upload Images'),
566 'done_text' => $main::locale->text('deleted')
568 push @sources , $attdata;
570 $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources count=".scalar(@sources));
584 SL::Controller::File - Controller for managing files
591 # The Controller is called direct from the webpages
594 <a href="controller.pl?action=File/list&file_type=document\
595 &object_type=[% HTML.escape(type) %]&object_id=[% HTML.url(id) %]">
598 # or indirect via javascript functions from js/kivi.File.js
601 kivi.popup_dialog({ url: 'controller.pl',
602 data: { action : 'File/ajax_upload',
603 file_type : 'uploaded',
614 This is a controller for handling files in a storage independent way.
615 The storage may be a Filesystem,a WebDAV, a Database or DMS.
616 These backends must be configered in ClientConfig.
617 This Controller use as intermediate layer for storage C<SL::File>.
619 The Controller is responsible to display forms for displaying the files at the ERP-objects and
620 for uploading and downloading the files.
622 More description of the intermediate layer see L<SL::File>.
626 =head2 C<action_list>
628 This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call.
629 Dependent of file_type different sources are available.
631 For documents there are the 'created' source and the imports from scanners or email.
632 For attachments and images only the 'uploaded' source available.
634 Available C<FORM PARAMS>:
638 =item C<form.object_id>
640 The Id of the ERP-object.
642 =item C<form.object_type>
644 The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller.
646 =item C<form.file_type>
648 For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images".
649 This file_type is a filter for the list.
653 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.
658 =head2 C<action_ajax_upload>
661 A new file or more files can selected by a dialog and insert into the system.
664 Available C<FORM PARAMS>:
668 =item C<form.file_type>
670 This parameter describe here the source for a new file :
671 "attachments" and "images"
673 This is a normal upload selection, which may be more then one file to upload.
675 =item C<form.object_id>
679 =item C<form.object_type>
681 are the same as at C<action_list>
685 =head2 C<action_ajax_files_uploaded>
687 The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS".
688 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?).
689 If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened.
691 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.
693 Available C<FORM PARAMS>:
697 =item C<form.ATTACHMENTS.uploadfiles>
699 This is an array of elements which have {filename} for the name and {data} for the contents.
701 Also object_id, object_type and file_type
705 =head2 C<action_download>
707 This is the real download of a file normally called via javascript "$.download("controller.pl", data);"
709 Available C<FORM PARAMS>:
713 Also object_id, object_type and file_type
717 =head2 C<action_ajax_importdialog>
719 A Dialog with all available and not imported files to import is open.
720 More then one file can be selected.
722 Available C<FORM PARAMS>:
728 The name of the source like "scanner1" or "email"
732 The full path to the directory on the server, where the files to import can found
734 Also object_id, object_type and file_type
738 =head2 C<action_ajax_delete>
740 Some files can be deleted
742 Available C<FORM PARAMS>:
748 The ids of the files to delete. Only this files are deleted not all versions of a file if the exists
752 =head2 C<action_ajax_unimport>
754 Some files can be unimported, dependent of the source of the file. This means they are moved
755 back to the directory of the source
757 Available C<FORM PARAMS>:
763 The ids of the files to unimport. Only this files are unimported not all versions of a file if the exists
767 =head2 C<action_ajax_rename>
769 One file can be renamed. There can be some checks if the same filename still exists at one object.
774 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>