From: Martin Helmling martin.helmling@octosoft.eu Date: Wed, 25 Jan 2017 14:52:27 +0000 (+0100) Subject: Dateimanagement: Controller zum Laden und Generierung der Dateien X-Git-Tag: release-3.5.4~1594 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=0bfbcce6e77e0f9d83e4f54f3fe9da7edcc866f3;p=kivitendo-erp.git Dateimanagement: Controller zum Laden und Generierung der Dateien sowie die dazugehörenden Templates --- diff --git a/SL/Controller/File.pm b/SL/Controller/File.pm new file mode 100644 index 000000000..05884dd4f --- /dev/null +++ b/SL/Controller/File.pm @@ -0,0 +1,770 @@ +package SL::Controller::File; + +use strict; + +use parent qw(SL::Controller::Base); + +use List::Util qw(first max); + +use utf8; +use Encode qw(decode); +use URI::Escape; +use Cwd; +use DateTime; +use File::stat; +use File::Spec::Unix; +use File::Spec::Win32; +use File::MimeInfo::Magic; +use SL::DB::Helper::Mappings; +use SL::DB::Order; +use SL::DB::DeliveryOrder; +use SL::DB::Invoice; + +use SL::DB::PurchaseInvoice; +use SL::DB::Part; +use SL::DB::GLTransaction; +use SL::DB::Draft; +use SL::DB::History; +use SL::JSON; +use SL::Helper::CreatePDF qw(:all); +use SL::Locale::String; +use SL::SessionFile; +use SL::File; +use SL::Controller::Helper::ThumbnailCreator qw(file_probe_image_type); + +use constant DO_DELETE => 0; +use constant DO_UNIMPORT => 1; + + +use Rose::Object::MakeMethods::Generic +( + 'scalar --get_set_init' => [ qw() ], + 'scalar' => [ qw(object object_type object_model object_id object_right file_type files is_global existing) ], +); + +__PACKAGE__->run_before('check_object_params', only => [ qw(list ajax_delete ajax_importdialog ajax_import ajax_unimport ajax_upload ajax_files_uploaded) ]); + +my %file_types = ( + 'sales_quotation' => { gen => 1 ,gltype => '', dir =>'SalesQuotation', model => 'Order', right => 'import_ar' }, + 'sales_order' => { gen => 1 ,gltype => '', dir =>'SalesOrder', model => 'Order', right => 'import_ar' }, + 'sales_delivery_order' => { gen => 1 ,gltype => '', dir =>'SalesDeliveryOrder', model => 'DeliveryOrder', right => 'import_ar' }, + 'invoice' => { gen => 1 ,gltype => 'ar', dir =>'SalesInvoice', model => 'Invoice', right => 'import_ar' }, + 'credit_note' => { gen => 1 ,gltype => '', dir =>'CreditNote', model => 'Invoice', right => 'import_ar' }, + 'request_quotation' => { gen => 3 ,gltype => '', dir =>'RequestForQuotation', model => 'Order', right => 'import_ap' }, + 'purchase_order' => { gen => 3 ,gltype => '', dir =>'PurchaseOrder', model => 'Order', right => 'import_ap' }, + 'purchase_delivery_order' => { gen => 3 ,gltype => '', dir =>'PurchaseDeliveryOrder',model => 'DeliveryOrder', right => 'import_ap' }, + 'purchase_invoice' => { gen => 2 ,gltype => 'ap', dir =>'PurchaseInvoice', model => 'PurchaseInvoice',right => 'import_ap' }, + 'vendor' => { gen => 0 ,gltype => '', dir =>'Vendor', model => 'Vendor', right => 'xx' }, + 'customer' => { gen => 1 ,gltype => '', dir =>'Customer', model => 'Customer', right => 'xx' }, + 'part' => { gen => 0 ,gltype => '', dir =>'Part', model => 'Part', right => 'xx' }, + 'gl_transaction' => { gen => 2 ,gltype => 'gl', dir =>'GeneralLedger', model => 'GLTransaction', right => 'import_ap' }, + 'draft' => { gen => 0 ,gltype => '', dir =>'Draft', model => 'Draft', right => 'xx' }, + 'csv_customer' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Customer', right => 'xx' }, + 'csv_vendor' => { gen => 1 ,gltype => '', dir =>'Reports', model => 'Vendor', right => 'xx' }, +); + +#--- 4 locale ---# +# $main::locale->text('imported') + +# +# actions +# + +sub action_list { + my ($self) = @_; + + my $isjson = 0; + $isjson = 1 if $::form->{json}; + + $self->_do_list($isjson); +} + +sub action_ajax_importdialog { + my ($self) = @_; + $::auth->assert($self->object_right); + my $path = $::form->{path}; + my @files = $self->_get_from_import($path); + my $source = { + 'name' => $::form->{source}, + 'path' => $path , + 'chk_action' => $::form->{source}.'_import', + 'chk_title' => $main::locale->text('Import scanned documents'), + 'chkall_title' => $main::locale->text('Import all'), + 'files' => \@files + }; + $self->render('file/import_dialog', + { layout => 0 + }, + source => $source + ); +} + +sub action_ajax_import { + my ($self) = @_; + $::auth->assert($self->object_right); + my $ids = $::form->{ids}; + my $source = $::form->{source}; + my $path = $::form->{path}; + my @files = $self->_get_from_import($path); + foreach my $filename (@{ $::form->{$ids} || [] }) { + my ($file,undef) = grep { $_->{name} eq $filename } @files; + if ( $file ) { + my $obj = SL::File->save(object_id => $self->object_id, + object_type => $self->object_type, + mime_type => 'application/pdf', + source => $source, + file_type => 'document', + file_name => $file->{filename}, + file_path => $file->{path} + ); + unlink($file->{path}) if $obj; + } + } + $self->_do_list(1); +} + +sub action_ajax_delete { + my ($self) = @_; + $self->_delete_all(DO_DELETE,$::locale->text('Following files are deleted:')); +} + +sub action_ajax_unimport { + my ($self) = @_; + $self->_delete_all(DO_UNIMPORT,$::locale->text('Following files are unimported:')); +} + +sub action_ajax_rename { + my ($self) = @_; + my $file = SL::File->get(id => $::form->{id}); + if ( ! $file ) { + $self->js->flash('error',$::locale->text('File not exists !'))->render(); + return; + } + $main::lxdebug->message(LXDebug->DEBUG2(), "object_id=".$file->object_id." object_type=".$file->object_type." dbfile=".$file); + my $sessionfile = $::form->{sessionfile}; + $main::lxdebug->message(LXDebug->DEBUG2(), "sessionfile=".$sessionfile); + if ( $sessionfile && -f $sessionfile ) { + $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file->file_name." to=".$::form->{to}." sessionfile=".$sessionfile); + # new uploaded file + if ( $::form->{to} eq $file->file_name ) { + # no rename so use as new version + $file->save_file($sessionfile); + $self->js->flash('warning',$::locale->text('File \'#1\' is used as new Version !',$file->file_name)); + + } else { + # new filename so it is a new file with same attributes as old file + eval { + SL::File->save(object_id => $file->object_id, + object_type => $file->object_type, + mime_type => $file->mime_type, + source => $file->source, + file_type => $file->file_type, + file_name => $::form->{to}, + file_path => $sessionfile + ); + unlink($sessionfile); + 1; + } or do { + $self->js->flash( 'error', t8('internal error (see details)')) + ->flash_detail('error', $@)->render; + return; + } + } + + } else { + # normal rename + eval { + my $res = $file->rename($::form->{to}); + $main::lxdebug->message(LXDebug->DEBUG2(), "rename result=".$res); + if ($res > SL::File::RENAME_OK) { + $self->js->flash('error', + $res == SL::File::RENAME_EXISTS ? $::locale->text('File still exists !') + : $res == SL::File::RENAME_SAME ? $::locale->text('Same Filename !') + : $::locale->text('File not exists !'))->render; + return 1; + } + 1; + } or do { + $self->js->flash( 'error', t8('internal error (see details)')) + ->flash_detail('error', $@)->render; + return; + } + } + $self->is_global($::form->{is_global}); + $self->file_type( $file->file_type); + $self->object_type($file->object_type); + $self->object_id( $file->object_id); + #$self->object_model($file_types{$file->module}->{model}); + #$self->object_right($file_types{$file->module}->{right}); + if ( $::form->{next_ids} ) { + my @existing = split(/,/, $::form->{next_ids}); + $self->existing(\@existing); + } + $self->_do_list(1); +} + +sub action_ajax_upload { + my ($self) = @_; + $self->{maxsize} = $::instance_conf->get_doc_max_filesize; + $self->{accept_types} = ''; + $self->{accept_types} = 'image/png,image/gif,image/jpeg,image/tiff,*png,*gif,*.jpg,*.tif' if $self->{file_type} eq 'image'; + $self->render('file/upload_dialog', + { layout => 0 + }, + ); +} + +sub action_ajax_files_uploaded { + my ($self) = @_; + + my $source = 'uploaded'; + $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload UPLOAD=".$::form->{ATTACHMENTS}->{uploadfiles}); + my @existing; + if ( $::form->{ATTACHMENTS}->{uploadfiles} ) { + my @upfiles = @{ $::form->{ATTACHMENTS}->{uploadfiles} }; + foreach my $idx (0 .. scalar(@upfiles) - 1) { + eval { + my $fname = uri_unescape($upfiles[$idx]->{filename}); + $main::lxdebug->message(LXDebug->DEBUG2(), "file_upload name=".$fname); + ## normalize and find basename + # first split with unix rules + # after that split with windows rules + my ($volume,$directories,$basefile) = File::Spec::Unix->splitpath($fname); + ($volume,$directories,$basefile) = File::Spec::Win32->splitpath($basefile); + + # to find real mime_type by magic we must save the filedata + + my $sess_fname = "file_upload_".$self->object_type."_".$self->object_id."_".$idx; + my $sfile = SL::SessionFile->new($sess_fname, mode => 'w'); + + $sfile->fh->print(${$upfiles[$idx]->{data}}); + $sfile->fh->close; + my $mime_type = File::MimeInfo::Magic::magic($sfile->file_name); + + if (! $mime_type) { + # if filename has the suffix "pdf", but is really no pdf set mimetype for no suffix + $mime_type = File::MimeInfo::Magic::mimetype($basefile); + $mime_type = 'application/octet-stream' if $mime_type eq 'application/pdf' || !$mime_type; + } + $main::lxdebug->message(LXDebug->DEBUG2(), "mime_type=".$mime_type); + if ( $self->file_type eq 'image' && $self->file_probe_image_type($mime_type, $basefile)) { + next; + } + my ($existobj) = SL::File->get_all(object_id => $self->object_id, + object_type => $self->object_type, + mime_type => $mime_type, + source => $source, + file_type => $self->file_type, + file_name => $basefile, + ); + + $main::lxdebug->message(LXDebug->DEBUG2(), "store1 exist=".$existobj); + if ($existobj) { + $main::lxdebug->message(LXDebug->DEBUG2(), "id=".$existobj->id." sessionfile=". $sfile->file_name); + push @existing, $existobj->id.'_'.$sfile->file_name; + } else { + my $fileobj = SL::File->save(object_id => $self->object_id, + object_type => $self->object_type, + mime_type => $mime_type, + source => $source, + file_type => $self->file_type, + file_name => $basefile, + ## two possibilities: what is better ? content or sessionfile ?? + #file_contents => ${$upfiles[$idx]->{data}}, + file_path => $sfile->file_name + ); + $main::lxdebug->message(LXDebug->DEBUG2(), "obj=".$fileobj); + unlink($sfile->file_name); + } + 1; + } or do { + $self->js->flash( 'error', t8('internal error (see details)')) + ->flash_detail('error', $@)->render; + return; + } + } + } + $self->existing(\@existing); + $self->_do_list(1); +} + +sub action_download { + my ($self) = @_; + my ($id,$version) = split /_/, $::form->{id}; + my $file = SL::File->get(id => $id ); + $file->version($version) if $version; + my $ref = $file->get_content; + if ( $file && $ref ) { + return $self->send_file($ref, + type => $file->mime_type, + name => $file->file_name, + ); + } +} + +# +# filters +# + +sub check_object_params { + my ($self) = @_; + + my $id = $::form->{object_id} +0; + my $draftid = $::form->{draft_id} +0; + my $gldoc = 0; + my $type = undef; + + if ( $draftid == 0 && $id == 0 && $::form->{is_global} ) { + $gldoc = 1; + $type = $::form->{object_type}; + } + elsif ( $id == 0 ) { + $id = $::form->{draft_id}; + $type = 'draft'; + } elsif ( $::form->{object_type} ) { + $type = $::form->{object_type}; + } + die "No object type" if ! $type; + die "No file type" if ! $::form->{file_type}; + die "Unkown object type" if ! $file_types{$type}; + + $self->is_global($gldoc); + $self->file_type($::form->{file_type}); + $self->object_type($type); + $self->object_id($id); + $self->object_model($file_types{$type}->{model}); + $self->object_right($file_types{$type}->{right}); + $main::lxdebug->message(LXDebug->DEBUG2(), "checked: object_id=".$self->object_id." object_type=".$self->object_type." is_global=".$self->is_global); + + # $::auth->assert($self->object_right); + + # my $model = 'SL::DB::' . $self->object_model; + # $self->object($model->new(id => $self->object_id)->load || die "Record not found"); + + return 1; +} + +# +# private methods +# + +sub _delete_all { + my ($self,$do_unimport,$infotext) = @_; + my $files = ''; + my $ids = $::form->{ids}; + foreach my $id_version (@{ $::form->{$ids} || [] }) { + my ($id,$version) = split /_/, $id_version; + my $dbfile = SL::File->get(id => $id); + $dbfile->version($version) if $dbfile && $version; + if ( $dbfile && $dbfile->delete ) { + $files .= ' '.$dbfile->file_name; + } + } + $self->js->flash('info',$infotext.$files) if $files; + $self->_do_list(1); +} + +sub _do_list { + my ($self,$json) = @_; + my @files; + $main::lxdebug->message(LXDebug->DEBUG2(), "do_list: object_id=".$self->object_id." object_type=".$self->object_type." file_type=".$self->file_type." json=".$json); + if ( $self->file_type eq 'document' ) { + @files = SL::File->get_all_versions(object_id => $self->object_id , + object_type => $self->object_type, + file_type => $self->file_type ); + + $main::lxdebug->message(LXDebug->DEBUG2(), "cnt1=".scalar(@files)); + } + elsif ( $self->file_type eq 'attachment' || $self->file_type eq 'image' ) { + @files = SL::File->get_all(object_id => $self->object_id , + object_type => $self->object_type, + file_type => $self->file_type ); + $main::lxdebug->message(LXDebug->DEBUG2(), "cnt2=".scalar(@files)); + } + $self->files(\@files); + $self->_mk_render('file/list',1,0,$json); +} + +sub _get_from_import { + my ($self,$path) = @_; + my @foundfiles ; + + $main::lxdebug->message(LXDebug->DEBUG2(), "import path=".$path); + my $language = $::lx_office_conf{system}->{language}; + my $timezone = $::locale->get_local_time_zone()->name; + if (opendir my $dir, $path) { + my @files = ( readdir $dir); + foreach my $file ( @files) { + next if (($file eq '.') || ($file eq '..')); + $file = Encode::decode('utf-8', $file); + $main::lxdebug->message(LXDebug->DEBUG2(), "file=".$file); + + next if( -d "$path/$file"); + + my $tmppath = File::Spec->catfile( $path, $file ); + $main::lxdebug->message(LXDebug->DEBUG2(), "tmppath=".$tmppath." file=".$file); + next if( ! -f $tmppath); + + my $st = stat($tmppath); + my $dt = DateTime->from_epoch( epoch => $st->mtime, time_zone => $timezone, locale => $language); + my $sname = $main::locale->quote_special_chars('HTML',$file); + push @foundfiles , { + 'name' => $file, + 'filename' => $sname, + 'path' => $tmppath, + 'mtime' => $st->mtime, + 'date' => $dt->dmy('.')." ".$dt->hms, + }; + + } + } + $main::lxdebug->message(LXDebug->DEBUG2(), "return ".scalar(@foundfiles)." files"); + return @foundfiles; +} + +sub _mk_render { + my ($self,$template,$edit,$scanner,$json) = @_; + my $err; + eval { + ##TODO here a configurable code must be implemented + + my $title; + $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: object_id=".$self->object_id." object_type=".$self->object_type. + " file_type=".$self->file_type." json=".$json." filecount=".scalar(@{ $self->files })." is_global=".$self->is_global); + my @sources = $self->_get_sources(); + foreach my $source ( @sources ) { + $main::lxdebug->message(LXDebug->DEBUG2(), "mk_render: source name=".$source->{name}); + @{$source->{files}} = grep { $_->source eq $source->{name}} @{ $self->files }; + } + if ( $self->file_type eq 'document' ) { + $title = $main::locale->text('Documents'); + } elsif ( $self->file_type eq 'attachment' ) { + $title = $main::locale->text('Attachments'); + } elsif ( $self->file_type eq 'image' ) { + $title = $main::locale->text('Images'); + } + + my $output = SL::Presenter->get->render( + $template, + title => $title, + SOURCES => \@sources, + edit_attachments => $edit, + object_type => $self->object_type, + object_id => $self->object_id, + file_type => $self->file_type, + is_global => $self->is_global, + json => $json, + ); + if ( $json ) { + $self->js->html('#'.$self->file_type.'_list_'.$self->object_type, $output); + if ( $self->existing && scalar(@{$self->existing}) > 0) { + my $first = shift @{$self->existing}; + my ($first_id,$sfile) = split('_',$first,2); + #$main::lxdebug->message(LXDebug->DEBUG2(), "id=".$first_id." sessionfile=". $sfile); + my $file = SL::File->get(id => $first_id ); + $self->js->run('kivi.File.askForRename',$first_id,$file->file_name,$sfile,join (',', @{$self->existing}), $self->is_global); + } + $self->js->render(); + } else { + $self->render(\$output, { layout => 0, process => 0 }); + } + 1; + } or do { + if ($json ){ + $self->js->flash( 'error', t8('internal error (see details)')) + ->flash_detail('error', $@)->render; + } else { + $self->render('generic/error', { layout => 0 }, label_error => $@); + } + }; +} + + +sub _get_sources { + my ($self) = @_; + my @sources; + $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources file_type=". $self->file_type); + if ( $self->file_type eq 'document' ) { + ##TODO statt gen neue attribute in filetypes : + if (($file_types{$self->object_type}->{gen}*1 & 1)==1) { + my $gendata = { + 'name' => 'created', + 'title' => $main::locale->text('generated Files'), + 'chk_action' => 'documents_delete', + 'chk_title' => $main::locale->text('Delete Documents'), + 'chkall_title' => $main::locale->text('Delete all'), + 'file_title' => $main::locale->text('filename'), + 'confirm_text' => $main::locale->text('delete'), + 'can_rename' => 1, + 'rename_title' => $main::locale->text('Rename Documents'), + 'done_text' => $main::locale->text('deleted') + }; + push @sources , $gendata; + } + if (($file_types{$self->object_type}->{gen}*1 & 2)==2) { + my @others = SL::File->get_other_sources(); + $main::lxdebug->message(LXDebug->DEBUG2(), "other cnt=". scalar(@others)); + foreach my $scanner_or_mailrx (@others) { + my $other = { + 'name' => $scanner_or_mailrx->{name}, + 'title' => $main::locale->text('from \'#1\' imported Files',$scanner_or_mailrx->{description}), + 'chk_action' => $scanner_or_mailrx->{name}.'_unimport', + 'chk_title' => $main::locale->text('Unimport documents'), + 'chkall_title' => $main::locale->text('Unimport all'), + 'file_title' => $main::locale->text('filename'), + 'confirm_text' => $main::locale->text('unimport'), + 'can_rename' => 1, + 'rename_title' => $main::locale->text('Rename Documents'), + 'can_import' => 1, + 'import_title' => $main::locale->text('Add Document from \'#1\'',$scanner_or_mailrx->{name}), + 'path' => $scanner_or_mailrx->{directory}, + 'done_text' => $main::locale->text('unimported') + }; + push @sources , $other; + } + } + } + elsif ( $self->file_type eq 'attachment' ) { + my $attdata = { + 'name' => 'uploaded', + 'title' => $main::locale->text(''), + 'chk_action' => 'attachments_delete', + 'chk_title' => $main::locale->text('Delete Attachments'), + 'chkall_title' => $main::locale->text('Delete all'), + 'file_title' => $main::locale->text('filename'), + 'confirm_text' => $main::locale->text('delete'), + 'can_rename' => 1, + 'are_existing' => $self->existing ? 1 : 0, + 'rename_title' => $main::locale->text('Rename Attachments'), + 'can_upload' => 1, + 'upload_title' => $main::locale->text('Upload Attachments'), + 'done_text' => $main::locale->text('deleted') + }; + push @sources , $attdata; + } + elsif ( $self->file_type eq 'image' ) { + my $attdata = { + 'name' => 'uploaded', + 'title' => $main::locale->text(''), + 'chk_action' => 'images_delete', + 'chk_title' => $main::locale->text('Delete Images'), + 'chkall_title' => $main::locale->text('Delete all'), + 'file_title' => $main::locale->text('filename'), + 'confirm_text' => $main::locale->text('delete'), + 'can_rename' => 1, + 'are_existing' => $self->existing ? 1 : 0, + 'rename_title' => $main::locale->text('Rename Images'), + 'can_upload' => 1, + 'upload_title' => $main::locale->text('Upload Images'), + 'done_text' => $main::locale->text('deleted') + }; + push @sources , $attdata; + } + $main::lxdebug->message(LXDebug->DEBUG2(), "get_sources count=".scalar(@sources)); + return @sources; +} + +1; + +__END__ + +=pod + +=encoding utf-8 + +=head1 NAME + +SL::Controller::File - Controller for managing files + + +=head1 SYNOPSIS + +=begin text + + # The Controller is called direct from the webpages + + + + + + # or indirect via javascript functions from js/kivi.File.js + + + kivi.popup_dialog({ url: 'controller.pl', + data: { action : 'File/ajax_upload', + file_type : 'uploaded', + object_type: type, + object_id : id + } + ... + +=end text + + +=head1 DESCRIPTION + +This is a controller for handling files in a storage independant way. +The storage may be a Filesystem,a WebDAV, a Database or DMS. +These backends must be configered in ClientConfig. +This Controller use as intermediate layer for storage C. + +The Controller is responsible to display forms for displaying the files at the ERP-objects and +for uploading and downloading the files. + +More description of the intermediate layer see L. + +=head1 METHODS + +=head2 C + +This loads a list of files on a webpage. This can be done with a normal submit or via an ajax/json call. +Dependant of file_type different sources are available. + +For documents there are the 'created' source and the imports from scanners or email. +For attachments and images only the 'uploaded' source available. + +Available C
: + +=over 4 + +=item C + +The Id of the ERP-object. + +=item C + +The Type of the ERP-object like "sales_quotation". A clear mapping to the class/model exists in the controller. + +=item C + +For one ERP-object may exists different type of documents the type may be "documents","attachments" or "images". +This file_type is a filter for the list. + +=item C + +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. + +=back + + +=head2 C + + +A new file or more files can selected by a dialog and insert into the system. + + +Available C: + +=over 4 + +=item C + +This parameter describe here the source for a new file : +"attachments" and "images" + +This is a normal upload selection, which may be more then one file to upload. + +=item C + +and + +=item C + +are the same as at C + +=back + +=head2 C + +The Upload of selected Files. The "multipart_formdata" is parsed in SL::Request into the formsvariable "form.ATTACHMENTS". +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?). +If the same filename still exists at this object after the download for each existing filename a rename dialog will be opened. + +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. + +Available C: + +=over 4 + +=item C + +This is an array of elements which have {filename} for the name and {data} for the contents. + +Also object_id, object_type and file_type + +=back + +=head2 C + +This is the real download of a file normally called via javascript "$.download("controller.pl", data);" + +Available C: + +=over 4 + +Also object_id, object_type and file_type + +=back + +=head2 C + +A Dialog with all available and not imported files to import is open. +More then one file can be selected. + +Available C: + +=over 4 + +=item C + +The name of the source like "scanner1" or "email" + +=item C + +The full path to the directory on the server, where the files to import can found + +Also object_id, object_type and file_type + +=back + +=head2 C + +Some files can be deleted + +Available C: + +=over 4 + +=item C + +The ids of the files to delete. Only this files are deleted not all versions of a file if the exists + +=back + +=head2 C + +Some files can be unimported, dependant of the source of the file. This means they are moved +back to the directory of the source + +Available C: + +=over 4 + +=item C + +The ids of the files to unimport. Only this files are unimported not all versions of a file if the exists + +=back + +=head2 C + +One file can be renamed. There can be some checks if the same filename still exists at one object. + + +=head1 AUTHOR + +Martin Helmling Emartin.helmling@opendynamic.deE + +=cut + diff --git a/SL/Controller/Helper/ThumbnailCreator.pm b/SL/Controller/Helper/ThumbnailCreator.pm new file mode 100644 index 000000000..072d89d76 --- /dev/null +++ b/SL/Controller/Helper/ThumbnailCreator.pm @@ -0,0 +1,144 @@ +package SL::Controller::Helper::ThumbnailCreator; + +use strict; + +use SL::Locale::String qw(t8); +use Carp; +use GD; +use Image::Info; +use File::MimeInfo::Magic; +use List::MoreUtils qw(apply); +use List::Util qw(max); +use Rose::DB::Object::Util; + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT = qw(file_create_thumbnail file_update_thumbnail file_probe_type file_probe_image_type file_update_type_and_dimensions); + +# TODO PDFs and others like odt,txt,... +our %supported_mime_types = ( + 'image/gif' => { extension => 'gif', convert_to_png => 1, }, + 'image/png' => { extension => 'png' }, + 'image/jpeg' => { extension => 'jpg' }, + 'image/tiff' => { extension => 'tif'}, +); + +sub file_create_thumbnail { + my ($self) = @_; + croak "No picture set yet" if !$self->file_content; + + my $image = GD::Image->new($self->file_content); + my ($width, $height) = $image->getBounds; + my $max_dim = 64; + my $curr_max = max $width, $height, 1; + my $factor = $curr_max <= $max_dim ? 1 : $curr_max / $max_dim; + my $new_width = int($width / $factor + 0.5); + my $new_height = int($height / $factor + 0.5); + my $thumbnail = GD::Image->new($new_width, $new_height); + + $thumbnail->copyResized($image, 0, 0, 0, 0, $new_width, $new_height, $width, $height); + + $self->thumbnail_img_content($thumbnail->png); + $self->thumbnail_img_content_type('image/png'); + $self->thumbnail_img_width($new_width); + $self->thumbnail_img_height($new_height); + return 1; + +} + +sub file_update_thumbnail { + my ($self) = @_; + + return 1 if !$self->file_content || !$self->file_content_type || !Rose::DB::Object::Util::get_column_value_modified($self, 'file_content'); + $self->file_create_thumbnail; + return 1; +} + +sub file_probe_image_type { + my ($self, $mime_type, $basefile) = @_; + + if ( !$supported_mime_types{ $mime_type } ) { + $self->js->flash('error',t8('file \'#1\' has unsupported image type \'#2\' (supported types: #3)', + $basefile, $mime_type, join(' ', sort keys %supported_mime_types))); + return 1; + } + return 0; +} + +sub file_probe_type { + my ($self) = @_; + + return (t8("No file uploaded yet")) if !$self->file_content; + my $mime_type = File::MimeInfo::Magic::magic($self->file_content); + + my $info = Image::Info::image_info(\$self->{file_content}); + if (!$info || $info->{error} || !$info->{file_media_type} || !$supported_mime_types{ $info->{file_media_type} }) { + $::lxdebug->warn("Image::Info error: " . $info->{error}) if $info && $info->{error}; + return (t8('Unsupported image type (supported types: #1)', join(' ', sort keys %supported_mime_types))); + } + + $self->file_content_type($info->{file_media_type}); + $self->files_img_width($info->{width}); + $self->files_img_height($info->{height}); + $self->files_mtime(DateTime->now_local); + + $self->file_create_thumbnail; + + return (); +} + +sub file_update_type_and_dimensions { + my ($self) = @_; + + return () if !$self->file_content; + return () if $self->file_content_type && $self->files_img_width && $self->files_img_height && !Rose::DB::Object::Util::get_column_value_modified($self, 'file_content'); + + my @errors = $self->file_probe_type; + return @errors if @errors; + + my $info = $supported_mime_types{ $self->file_content_type }; + if ($info->{convert_to_png}) { + $self->file_content(GD::Image->new($self->file_content)->png); + $self->file_content_type('image/png'); + $self->filename(apply { s/\.[^\.]+$//; $_ .= '.png'; } $self->filename); + } + return (); +} + +1; +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + + SL::DB::Helper::ThumbnailCreator - DatabaseClass Helper for Fileuploads + +=head1 SYNOPSIS + + use SL::DB::Helper::ThumbnailCreator; + + # synopsis... + +=head1 DESCRIPTION + + # longer description.. + +=head1 AUTHOR + + Werner Hahn Ewh@futureworldsearch.netE + +=cut + + +=head1 INTERFACE + + +=head1 DEPENDENCIES + + +=head1 SEE ALSO + + diff --git a/SL/Form.pm b/SL/Form.pm index 344f1f0bc..f50a121d6 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -84,6 +84,8 @@ use URI; use List::Util qw(first max min sum); use List::MoreUtils qw(all any apply); use SL::DB::Tax; +use SL::Helper::File qw(:all); +use SL::Helper::CreatePDF qw(merge_pdfs); use strict; @@ -1076,9 +1078,18 @@ sub parse_template { # therefore copy to webdav, even if we do not have the webdav feature enabled (just archive) my $copy_to_webdav = $::instance_conf->get_webdav_documents && !$self->{preview} && $self->{tmpdir} && $self->{tmpfile} && $self->{type}; + if ( $ext_for_format eq 'pdf' && $::instance_conf->get_doc_storage ) { + $self->append_general_pdf_attachments(filepath => $self->{tmpdir}."/".$self->{tmpfile}, + type => $self->{type}); + } if ($self->{media} eq 'file') { copy(join('/', $self->{cwd}, $userspath, $self->{tmpfile}), $out =~ m|^/| ? $out : join('/', $self->{cwd}, $out)) if $template->uses_temp_file; Common::copy_file_to_webdav_folder($self) if $copy_to_webdav; + if (!$self->{preview} && $::instance_conf->get_doc_storage) + { + $self->{attachment_filename} ||= $self->generate_attachment_filename; + $self->store_pdf($self); + } $self->cleanup; chdir("$self->{cwd}"); @@ -1089,6 +1100,10 @@ sub parse_template { Common::copy_file_to_webdav_folder($self) if $copy_to_webdav; + if ( !$self->{preview} && $ext_for_format eq 'pdf' && $::instance_conf->get_doc_storage) { + $self->{attachment_filename} ||= $self->generate_attachment_filename; + $self->store_pdf($self); + } if ($self->{media} eq 'email') { my $mail = Mailer->new; @@ -2453,7 +2468,6 @@ sub get_name { } sub new_lastmtime { - $main::lxdebug->enter_sub(); my ($self, $table, $provided_dbh) = @_; @@ -2465,9 +2479,7 @@ sub new_lastmtime { my $ref = selectfirst_hashref_query($self, $dbh, $query, $self->{id}); $ref->{mtime} ||= $ref->{itime}; $self->{lastmtime} = $ref->{mtime}; - $main::lxdebug->message(LXDebug->DEBUG2(),"new lastmtime=".$self->{lastmtime}); - $main::lxdebug->leave_sub(); } sub mtime_ischanged { @@ -3000,6 +3012,7 @@ sub save_status { #--- 4 locale ---# # $main::locale->text('SAVED') +# $main::locale->text('SCREENED') # $main::locale->text('DELETED') # $main::locale->text('ADDED') # $main::locale->text('PAYMENT POSTED') @@ -3012,6 +3025,8 @@ sub save_status { # $main::locale->text('MAILED') # $main::locale->text('SCREENED') # $main::locale->text('CANCELED') +# $main::locale->text('IMPORT') +# $main::locale->text('UNIMPORT') # $main::locale->text('invoice') # $main::locale->text('proforma') # $main::locale->text('sales_order') diff --git a/SL/Helper/File.pm b/SL/Helper/File.pm new file mode 100644 index 000000000..7b31ebdd6 --- /dev/null +++ b/SL/Helper/File.pm @@ -0,0 +1,151 @@ +package SL::Helper::File; + +use strict; + +use Exporter 'import'; +our @EXPORT_OK = qw(store_pdf append_general_pdf_attachments); +our %EXPORT_TAGS = (all => \@EXPORT_OK,); +use SL::File; + +sub store_pdf { + my ($self, $form) = @_; + return unless $::instance_conf->get_doc_storage; + my $type = $form->{type}; + $type = $form->{formname} if $form->{formname} && !$form->{type}; + my $id = $form->{id}; + $id = $form->{attachment_id} if $form->{attachment_id} && !$form->{id}; + return if !$id || !$type; + my $prefix = $form->get_number_prefix_for_type(); + SL::File->save( + object_id => $id, + object_type => $type, + mime_type => 'application/pdf', + source => 'created', + file_type => 'document', + file_name => $form->{attachment_filename}, + file_path => $form->{tmpfile}, + file_number => $form->{"${prefix}number"}, + ); +} + +# This method also needed by $form to append all general pdf attachments +# +sub append_general_pdf_attachments { + my ($self, %params) = @_; + return 0 unless $::instance_conf->get_doc_storage; + return 0 if !$params{filepath} || !$params{type}; + + my @files = SL::File->get_all( + object_id => 0, + object_type => $params{type}, + mime_type => 'application/pdf' + ); + return 0 if $#files < 0; + + my @pdf_file_names = ($params{filepath}); + foreach my $file (@files) { + my $path = $file->get_file; + push @pdf_file_names, $path if $path; + } + + #TODO immer noch das alte Problem: + #je nachdem von woher der Aufruf kommt ist man in ./users oder . + my $savedir = POSIX::getcwd(); + chdir("$self->{cwd}"); + $self->merge_pdfs( + file_names => \@pdf_file_names, + out_path => $params{filepath} + ); + chdir("$savedir"); + + return 0; +} + +1; + +__END__ + +=encoding utf-8 + +=head1 NAME + +SL::Helper::File - Helper for $::Form to store generated PDF-Documents + + +=head1 SYNOPSIS + +# This Helper is used by SL::Form to store new generated PDF-Files and append general attachments to this documents. +# +# in SL::Form.pm: + + $self->store_pdf($self); + + $self->append_general_pdf_attachments($self) if ( $ext_for_format eq 'pdf' ); + +=head1 DESCRIPTION + +The files with file_type "generated" are stored. + +See also L. + +=head1 METHODS + + +=head2 C + +Copy generated PDF-File to File destination. +This method is need from SL::Form after LaTeX-PDF Generation + +=over 4 + +=item C + +ID of ERP-Document + +=item C + +type of ERP-document + +=item C + +if no type is set this is used as type + +=item C + +if no id is set this is used as id + +=item C + +The path of the generated PDF-file + +=item C + +The generated filename which is used as new filename (without timestamp) + +=back + +=head2 C + +This method also needed by SL::Form to append all general pdf attachments + +needed C: + +=over 4 + +=item C + +type of ERP-document + +=item C + +Name of file to which the general attachments must be added + +=back + +=head1 AUTHOR + +Martin Helmling Emartin.helmling@opendynamic.deE + + +=cut + diff --git a/js/kivi.File.js b/js/kivi.File.js new file mode 100644 index 000000000..036cefee6 --- /dev/null +++ b/js/kivi.File.js @@ -0,0 +1,242 @@ +namespace('kivi.File', function(ns) { + + ns.rename = function(id,type,file_type,checkbox_class,is_global) { + var checkboxes = $('.'+checkbox_class).filter(function () { return $(this).prop('checked'); }); + + if (checkboxes.size() === 0) { + alert(kivi.t8("No file selected, please set one checkbox!")); + return false; + } + if (checkboxes.size() > 1) { + alert(kivi.t8("More than one file selected, please set only one checkbox!")); + return false; + } + var file_id = checkboxes[0].value; + $('#newfilename_id').val($('#filename_'+file_id).text()); + $('#next_ids_id').val(''); + $('#is_global_id').val(is_global); + $('#rename_id_id').val(file_id); + $('#sessionfile_id').val(''); + $('#rename_extra_text').html(''); + kivi.popup_dialog({ + id: 'rename_dialog', + dialog: { title: kivi.t8("Rename attachment") + , width: 400 + , height: 200 + , modal: true } }); + return true; + } + + ns.renameclose = function() { + $("#rename_dialog").dialog('close'); + return false; + } + + ns.renameaction = function() { + $("#rename_dialog").dialog('close'); + var data = { + action: 'File/ajax_rename', + id: $('#rename_id_id').val(), + to: $('#newfilename_id').val(), + next_ids: $('#next_ids_id').val(), + is_global: $('#is_global_id').val(), + sessionfile: $('#sessionfile_id').val(), + }; + $.post("controller.pl", data, kivi.eval_json_result); + return true; + } + + ns.askForRename = function(file_id,file_name,sessionfile,next_ids,is_global) { + $('#newfilename_id').val(file_name); + $('#rename_id_id').val(file_id); + $('#is_global_id').val(is_global); + $('#next_ids_id').val(next_ids); + $('#sessionfile_id').val(sessionfile); + $('#rename_extra_text').html(kivi.t8("The uploaded filename still exists.
If you not modify the name this is a new version of the file")); + kivi.popup_dialog( + { + id: 'rename_dialog', + dialog: { title: kivi.t8("Rename attachment") + , width: 400 + , height: 200 + , modal: true } + }); + } + + ns.upload = function(id,type,filetype,upload_title,gl) { + kivi.popup_dialog({ url: 'controller.pl', + data: { action: 'File/ajax_upload', + file_type: filetype, + object_type: type, + object_id: id, + is_global: gl + }, + id: 'files_upload', + dialog: { title: upload_title, width: 650, height: 240 } }); + return true; + } + + ns.reset_upload_form = function() { + $('#attachment_updfile').val(''); + $("#upload_result").html(''); + ns.allow_upload_submit(); + } + + ns.allow_upload_submit = function() { + $('#upload_selected_button').prop('disabled',$('#upload_files').val() === ''); + } + + ns.upload_selected_files = function(id,type,filetype,maxsize,is_global) { + var myform = document.getElementById("upload_form"); + var filesize = 0; + var myfiles = document.getElementById("upload_files").files; + for ( i=0; i < myfiles.length; i++ ) { + var fname =''; + try { + filesize += myfiles[i].size; + fname = encodeURIComponent(myfiles[i].name); + } + catch(err) { + fname =''; + try { + fname = myfiles[i].name; + } + catch(err2) { fname ='';} + $("#upload_result").html(kivi.t8("filename has not uploadable characters ")+fname); + return; + } + } + if ( filesize > maxsize ) { + $("#upload_result").html(kivi.t8("filesize too big: ")+ + filesize+ kivi.t8(" bytes, max=") + maxsize ); + return; + } + + myform.action ="controller.pl?action=File/ajax_files_uploaded&json=1&object_type="+ + type+'&object_id='+id+'&file_type='+filetype+'&is_global='+is_global; + var oReq = new XMLHttpRequest(); + oReq.onload = ns.attSuccess; + oReq.upload.onprogress = ns.attProgress; + oReq.upload.onerror = ns.attFailed; + oReq.upload.onabort = ns.attCanceled; + oReq.open("post",myform.action, true); + $("#upload_result").html(kivi.t8("start upload")); + oReq.send(new FormData(myform)); + } + + ns.attProgress = function(oEvent) { + if (oEvent.lengthComputable) { + var percentComplete = (oEvent.loaded / oEvent.total) * 100; + $("#upload_result").html(percentComplete+" % "+ kivi.t8("uploaded")); + } + } + + ns.attFailed = function(evt) { + $('#files_upload').dialog('close'); + $("#upload_result").html(kivi.t8("An error occurred while transferring the file.")); + } + + ns.attCanceled = function(evt) { + $('#files_upload').dialog('close'); + $("#upload_result").html(kivi.t8("The transfer has been canceled by the user.")); + } + + ns.attSuccess = function() { + $('#files_upload').dialog('close'); + kivi.eval_json_result(jQuery.parseJSON(this.response)); + } + + ns.delete = function(id,type,file_type,checkbox_class,is_global) { + var checkboxes = $('.'+checkbox_class).filter(function () { return $(this).prop('checked'); }); + + if ((checkboxes.size() === 0) || + !confirm(kivi.t8('Do you really want to delete the selected documents?'))) + return false; + var data = { + action : 'File/ajax_delete', + object_id : id, + object_type: type, + file_type : file_type, + ids : checkbox_class, + is_global : is_global, + }; + $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result); + return false; + } + + ns.unimport = function(id,type,file_type,checkbox_class) { + var checkboxes = $('.'+checkbox_class).filter(function () { return $(this).prop('checked'); }); + + if ((checkboxes.size() === 0) || + !confirm(kivi.t8('Do you really want to unimport the selected documents?'))) + return false; + var data = { + action : 'File/ajax_unimport', + object_id : id, + object_type: type, + file_type : file_type, + ids : checkbox_class, + }; + $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result); + return false; + } + + ns.update = function(id,type,file_type,is_global) { + var data = { + action: 'File/list', + json: 1, + object_type: type, + object_id: id, + file_type: file_type, + is_global: is_global + }; + + $.post("controller.pl", data, kivi.eval_json_result); + return false; + } + + ns.import = function (id,type,file_type,fromwhere,frompath) { + kivi.popup_dialog({ url: 'controller.pl', + data: { action : 'File/ajax_importdialog', + object_type : type, + source : fromwhere, + path : frompath, + file_type : file_type, + object_id : id + }, + id: 'import_dialog', + dialog: { title: kivi.t8('Import documents from #1',[fromwhere]), width: 420, height: 540 } + }); + return true; + } + + ns.importclose = function() { + $("#import_dialog").dialog('close'); + return false; + } + + ns.importaction = function(id,type,file_type,fromwhere,frompath,checkbox_class) { + var checkboxes = $('.'+checkbox_class).filter(function () { return $(this).prop('checked'); }); + + $("#import_dialog").dialog('close'); + if (checkboxes.size() === 0) { + return false; + } + var data = { + action : 'File/ajax_import', + object_id : id, + object_type: type, + file_type : file_type, + source : fromwhere, + path : frompath, + ids : checkbox_class + }; + $.post("controller.pl?" + checkboxes.serialize(), data, kivi.eval_json_result); + return true; + } + + + ns.init = function() { + } + +}); diff --git a/locale/de/all b/locale/de/all index 052b63e8b..813284ad2 100644 --- a/locale/de/all +++ b/locale/de/all @@ -1475,6 +1475,8 @@ $self->{texts} = { 'Illegal date' => 'Ungültiges Datum', 'Image' => 'Grafik', 'Import' => 'Import', + 'Import AP from Scanner or Email' => 'Einkaufsbelege importieren vom Scanner oder von Email', + 'Import AR from Scanner or Email' => 'Verkaufsbelege importieren vom Scanner oder von Email', 'Import CSV' => 'CSV-Import', 'Import Status' => 'Import Status', 'Import a MT940 file:' => 'Laden Sie eine MT940 Datei hoch:', diff --git a/sql/Pg-upgrade2-auth/other_file_sources.sql b/sql/Pg-upgrade2-auth/other_file_sources.sql new file mode 100644 index 000000000..1f26477b7 --- /dev/null +++ b/sql/Pg-upgrade2-auth/other_file_sources.sql @@ -0,0 +1,7 @@ +-- @tag: other_file_sources +-- @description: Neue Gruppenrechte für das Importieren von Scannern oder email +-- @depends: release_3_4_0 master_rights_position_gaps +-- @locales: Import AP from Scanner or Email +-- @locales: Import AR from Scanner or Email +INSERT INTO auth.master_rights (position, name, description) VALUES (2050, 'import_ar', 'Import AR from Scanner or Email'); +INSERT INTO auth.master_rights (position, name, description) VALUES (2650, 'import_ap', 'Import AP from Scanner or Email'); diff --git a/templates/webpages/file/import_dialog.html b/templates/webpages/file/import_dialog.html new file mode 100644 index 000000000..48463eef5 --- /dev/null +++ b/templates/webpages/file/import_dialog.html @@ -0,0 +1,36 @@ +[%- USE L -%][%- USE LxERP -%][%- USE JavaScript -%] + + + + + + + + + + + + [%- FOREACH file = source.files %] + + + + + + [%- END %] + +
[% L.checkbox_tag(source.chk_action _ '_checkall') %][% source.chkall_title %][% LxERP.t8("Attached Filename") %]
[%- L.checkbox_tag(source.chk_action _ '[]', 'value'=file.name, 'class'=source.chk_action) %][% file.filename %]
+ +

+ [%- L.button_tag("kivi.File.importaction(" _ SELF.object_id _ ",'" _ SELF.object_type _ "','" _ SELF.file_type _ "','" _ source.name _ "','" _ source.path _ "','"_ source.chk_action _ "');", LxERP.t8('Continue'), id => "import_cont_btn") %] + [%- L.button_tag("kivi.File.importclose();" , LxERP.t8('Cancel') , class => "submit") %] +

+ + + + diff --git a/templates/webpages/file/list.html b/templates/webpages/file/list.html new file mode 100644 index 000000000..238fb12fe --- /dev/null +++ b/templates/webpages/file/list.html @@ -0,0 +1,101 @@ +[%- USE LxERP -%][% USE L %][% USE HTML %] +[%- IF ! json %] +
+[%- END %] +
[% title %]
+ +
+[%- SET can_rename = 0 %] +[%- FOREACH source = SOURCES %] + + + + + [%- SET checkname = source.chk_action %] + [%- IF is_global %] + [%- SET checkname = object_type _ '_' _ source.chk_action %] + [%- END %] + [%- IF edit_attachments %] + + + + [%- END %] + + + [%- IF file_type == 'image' %] + + + + [%- ELSE %] + + [%- END %] + + + + + [%- FOREACH file = source.files %] + + [%- IF edit_attachments %] + [%- IF file.newest %] + + [%- ELSE %] + + [%- END %] + + [%- END %] + + + [%- IF file_type == 'image' %] + + + + [%- ELSE %] + + [%- END %] + + [%- END %] + +
[% source.title %]
[% L.checkbox_tag(checkname _ '_checkall') %][% source.chkall_title %][% LxERP.t8('Date') %][% source.file_title %][% LxERP.t8('Title') %] + [% LxERP.t8('ImagePreview') %] + [% LxERP.t8('Description') %]
[%- L.checkbox_tag(checkname _ '[]', 'value'=file.id, 'class'=checkname) %][% file.mtime_as_timestamp_s %] + [% file.file_name %][% file.title %] + [% file.title %] + [% file.description %]
+ [%- IF edit_attachments %] + [%- IF source.can_import %] + [% L.button_tag("kivi.File.unimport(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "');",source.chk_title) %] + [%- ELSE %] + [% L.button_tag("kivi.File.delete(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "'," _ is_global _ ");", source.chk_title) %] + [%- END %] + [%- END %] + [%- IF source.can_rename %] + [%- can_rename = 1 %] + [% L.button_tag("kivi.File.rename(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ checkname _ "'," _ is_global _ ");", source.rename_title ) %] + [%- END %] + [%- IF source.can_upload %] + [% L.button_tag("kivi.File.upload(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.upload_title _ "'," _ is_global _ ");", source.upload_title ) %] + [%- END %] + [%- IF source.can_import %] + [% L.button_tag("kivi.File.import(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "','" _ source.name _ "','" _ source.path _"');", source.import_title ) %] + [%- END %] +
+[%- END %] +
+[% L.button_tag("kivi.File.update(" _ object_id _ ",'" _ object_type _ "','" _ file_type _ "'," _ is_global _ ");", LxERP.t8('Update')) %] +
+
+[%- IF ! json %] + +[%- UNLESS is_global %] +[%- IF can_rename %] +[% INCLUDE 'file/rename_dialog.html' -%] +[%- END %] +[%- END %] +[%- END %] diff --git a/templates/webpages/file/upload_dialog.html b/templates/webpages/file/upload_dialog.html new file mode 100644 index 000000000..eceb6b1d8 --- /dev/null +++ b/templates/webpages/file/upload_dialog.html @@ -0,0 +1,23 @@ +[%- USE L -%][%- USE LxERP -%][%- USE JavaScript -%] + +
+ + + + +
[%- LxERP.t8("Filename") %]: +
+ +

+ + [%- LxERP.t8("Reset") %] + [% LxERP.t8("Cancel") %] +

+ +
+ +

 

+ +
diff --git a/templates/webpages/part/form.html b/templates/webpages/part/form.html index 58af99839..0d7ab0838 100644 --- a/templates/webpages/part/form.html +++ b/templates/webpages/part/form.html @@ -22,10 +22,12 @@ [%- IF SELF.part.is_assembly %]
  • [% 'Assembly items' | $T8 %]
  • [%- END %] -[%- IF INSTANCE_CONF.get_doc_storage %] + [%- IF SELF.part.id %] + [%- IF INSTANCE_CONF.get_doc_storage %]
  • [% 'Attachments' | $T8 %]
  • [% 'Images' | $T8 %]
  • -[%- END %] + [%- END %] + [%- END %] [% IF SELF.all_languages.size %]
  • [% 'Translations' | $T8 %]
  • [% END %]