From 61dca5d346d9931a2665152049b9784adc83a9f0 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Sat, 4 May 2024 18:08:42 +0200 Subject: [PATCH 01/14] EmailJournal: bei 'application/octet-stream' versuche mime-Typ zu raten --- SL/Controller/EmailJournal.pm | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index 9a248c9b10..785bd1f693 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -32,6 +32,7 @@ use SL::DB::Manager::Vendor; use List::Util qw(first); use List::MoreUtils qw(any); +use File::MimeInfo::Magic; use Rose::Object::MakeMethods::Generic ( @@ -326,10 +327,21 @@ sub action_show_attachment { $::form->error(t8('You do not have permission to access this entry.')); } + my $mime_type = $attachment->mime_type; + if ($mime_type eq 'application/octet-stream') { + # try to guess the mime_type + if ($attachment->content =~ m/^%PDF/) { + $mime_type = 'application/pdf'; + } else { + $mime_type = File::MimeInfo::Magic::mimetype($attachment->name); + $mime_type = 'text/plain' if $mime_type eq 'application/octet-stream'; + } + } + return $self->send_file( \$attachment->content, name => $attachment->name, - type => $attachment->mime_type, + type => $mime_type, content_disposition => 'inline', ); } From 014942eaaa67edf5c310d70f4c332768a2a338b2 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Sat, 4 May 2024 18:48:46 +0200 Subject: [PATCH 02/14] EmailJournal: zeige nur Standarddateiformate in Dateivorschau --- SL/Controller/EmailJournal.pm | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index 785bd1f693..b56fbce4de 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -327,20 +327,33 @@ sub action_show_attachment { $::form->error(t8('You do not have permission to access this entry.')); } + my $name = $attachment->name; + my $content = \$attachment->content; my $mime_type = $attachment->mime_type; + + # try to guess the mime_type if ($mime_type eq 'application/octet-stream') { - # try to guess the mime_type if ($attachment->content =~ m/^%PDF/) { $mime_type = 'application/pdf'; } else { $mime_type = File::MimeInfo::Magic::mimetype($attachment->name); - $mime_type = 'text/plain' if $mime_type eq 'application/octet-stream'; + $mime_type ||= 'text/plain'; } } + # only show standard types + if (!any {$mime_type =~ m/^\Q$_\E/} qw( + text/plain text/xml image/png image/jpeg + application/pdf application/json application/xml + )) { + $content = \t8("Can't display file")->translated; + $name = ''; + $mime_type = 'text/plain'; + } + return $self->send_file( - \$attachment->content, - name => $attachment->name, + $content, + name => $name, type => $mime_type, content_disposition => 'inline', ); From 36f031f5c7743d7354ed1acbaf78e4e57274f0a5 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 16:13:48 +0200 Subject: [PATCH 03/14] =?UTF-8?q?EmailJournalAttachment:=20SQL:=20Verarbei?= =?UTF-8?q?tet-Flag=20hinzuf=C3=BCgen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/DB/MetaSetup/EmailJournalAttachment.pm | 1 + .../email_journal_attachments_add_processed_flag.sql | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 sql/Pg-upgrade2/email_journal_attachments_add_processed_flag.sql diff --git a/SL/DB/MetaSetup/EmailJournalAttachment.pm b/SL/DB/MetaSetup/EmailJournalAttachment.pm index 591e7992e2..7a37686064 100644 --- a/SL/DB/MetaSetup/EmailJournalAttachment.pm +++ b/SL/DB/MetaSetup/EmailJournalAttachment.pm @@ -18,6 +18,7 @@ __PACKAGE__->meta->columns( mtime => { type => 'timestamp', default => 'now()', not_null => 1 }, name => { type => 'text', not_null => 1 }, position => { type => 'integer', not_null => 1 }, + processed => { type => 'boolean', default => 'false', not_null => 1 }, ); __PACKAGE__->meta->primary_key_columns([ 'id' ]); diff --git a/sql/Pg-upgrade2/email_journal_attachments_add_processed_flag.sql b/sql/Pg-upgrade2/email_journal_attachments_add_processed_flag.sql new file mode 100644 index 0000000000..0ffaa36f55 --- /dev/null +++ b/sql/Pg-upgrade2/email_journal_attachments_add_processed_flag.sql @@ -0,0 +1,10 @@ +-- @tag: email_journal_attachments_add_processed_flag +-- @description: E-Mailanhänge als verarbeitet markieren +-- @depends: release_3_8_0 +ALTER TABLE email_journal_attachments ADD COLUMN processed BOOLEAN DEFAULT FALSE NOT NULL; + +-- set attachments of send emails to processed +UPDATE email_journal_attachments +SET processed = TRUE +FROM email_journal +WHERE email_journal_id = email_journal.id AND email_journal.status != 'imported' From fe445cb36cb47055a4b3f12864633c745b86e351 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 16:28:39 +0200 Subject: [PATCH 04/14] =?UTF-8?q?EmailJournalAttachment:=20Verarbeitet-Sta?= =?UTF-8?q?tus=20von=20Anh=C3=A4ngen=20anzeigen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locale/de/all | 1 + locale/en/all | 1 + templates/design40_webpages/email_journal/show.html | 2 ++ templates/webpages/email_journal/show.html | 2 ++ 4 files changed, 6 insertions(+) diff --git a/locale/de/all b/locale/de/all index 9b605249f7..2d1124de07 100755 --- a/locale/de/all +++ b/locale/de/all @@ -3066,6 +3066,7 @@ $ ./scripts/installation_check.pl', 'Private E-mail' => 'Private E-Mail', 'Private Phone' => 'Privates Tel.', 'Problem' => 'Problem', + 'Processed' => 'Verarbeitet', 'Processed attachments with function \'#1\':' => 'Anhänge verarbeitet mit Funktion \'#1\':', 'Processed successfully: ' => 'Erfolgreich verarbeitet: ', 'Produce' => 'Fertigen', diff --git a/locale/en/all b/locale/en/all index d0d22bad18..114f7cf95c 100755 --- a/locale/en/all +++ b/locale/en/all @@ -3065,6 +3065,7 @@ $self->{texts} = { 'Private E-mail' => '', 'Private Phone' => '', 'Problem' => '', + 'Processed' => '', 'Processed attachments with function \'#1\':' => '', 'Processed successfully: ' => '', 'Produce' => '', diff --git a/templates/design40_webpages/email_journal/show.html b/templates/design40_webpages/email_journal/show.html index 94e81ecf2e..0f3ad74f92 100644 --- a/templates/design40_webpages/email_journal/show.html +++ b/templates/design40_webpages/email_journal/show.html @@ -33,6 +33,7 @@

[% FORM.title %]

[% 'Attachment name' | $T8 %] [% 'MIME type' | $T8 %] [% 'Size' | $T8 %] + [% 'Processed' | $T8 %] @@ -41,6 +42,7 @@

[% FORM.title %]

[% L.link(SELF.url_for(action="download_attachment", id=attachment.id), attachment.name) %] [% HTML.escape(attachment.mime_type) %] [% HTML.escape(LxERP.format_amount(attachment.content.length, 0)) %] + [% HTML.escape(attachment.processed_as_bool_yn) %] [% END %] diff --git a/templates/webpages/email_journal/show.html b/templates/webpages/email_journal/show.html index 3809e1fca7..194be00cce 100644 --- a/templates/webpages/email_journal/show.html +++ b/templates/webpages/email_journal/show.html @@ -23,6 +23,7 @@

[% LxERP.t8("Attachments") %]

[% LxERP.t8("Attachment name") %] [% LxERP.t8("MIME type") %] [% LxERP.t8("Size") %] + [% LxERP.t8("Processed") %] @@ -32,6 +33,7 @@

[% LxERP.t8("Attachments") %]

[% L.link(SELF.url_for(action="download_attachment", id=attachment.id), attachment.name) %] [% HTML.escape(attachment.mime_type) %] [% HTML.escape(LxERP.format_amount(attachment.content.length, 0)) %] + [% HTML.escape(attachment.processed_as_bool_yn) %] [% END %] From 9cfa51dc2f8b2251a15770de62d883159e07361d Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 16:29:29 +0200 Subject: [PATCH 05/14] =?UTF-8?q?EmailJournalAttachment:=20verarbeitet=20w?= =?UTF-8?q?enn=20zum=20Belege=20hinzuf=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/DB/EmailJournalAttachment.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/SL/DB/EmailJournalAttachment.pm b/SL/DB/EmailJournalAttachment.pm index 820e0df4ac..92bef09681 100644 --- a/SL/DB/EmailJournalAttachment.pm +++ b/SL/DB/EmailJournalAttachment.pm @@ -61,6 +61,7 @@ sub add_file_to_record { die 'Storing the attachment file to the file management failed: ' . $@; }; } + $self->processed(1)->save; } From 34101eee892bca40a6d7bb55a94cba277d502e25 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 16:59:27 +0200 Subject: [PATCH 06/14] S:D:EmailJournal: entferne toten Code ZUGFeRD-Automatik wurde in SL::Helper::EmailProcessing verschoben --- SL/DB/EmailJournal.pm | 51 ------------------------------------------- 1 file changed, 51 deletions(-) diff --git a/SL/DB/EmailJournal.pm b/SL/DB/EmailJournal.pm index ed851f967e..d2ffe5fca0 100644 --- a/SL/DB/EmailJournal.pm +++ b/SL/DB/EmailJournal.pm @@ -57,57 +57,6 @@ sub linked { return !!scalar @{$self->linked_records}; } -sub process_attachments_as_purchase_invoices { - my ($self) = @_; - - my $attachments = $self->attachments_sorted; - foreach my $attachment (@$attachments) { - my $ap_invoice = $attachment->create_ap_invoice(); - next unless $ap_invoice; - - # link to email journal - $self->link_to_record($ap_invoice); - - # copy file to webdav folder - if ($::instance_conf->get_webdav_documents) { - my $webdav = SL::Webdav->new( - type => 'accounts_payable', - number => $ap_invoice->invnumber, - ); - my $webdav_file = SL::Webdav::File->new( - webdav => $webdav, - filename => $attachment->name, - ); - eval { - $webdav_file->store(data => \$attachment->content); - 1; - } or do { - die 'Storing the ZUGFeRD file to the WebDAV folder failed: ' . $@; - }; - } - # copy file to doc storage - if ($::instance_conf->get_doc_storage) { - eval { - SL::File->save( - object_id => $ap_invoice->id, - object_type => 'purchase_invoice', - mime_type => 'application/pdf', - source => 'uploaded', - file_type => 'document', - file_name => $attachment->name, - file_contents => $attachment->content, - ); - 1; - } or do { - die 'Storing the ZUGFeRD file in the storage backend failed: ' . $@; - }; - } - } - - my $new_ext_status = join('_', $self->extended_status, 'processed'); - $self->update({ extended_status => $new_ext_status}); -} - 1; __END__ From da42fc524e053d07b899583a487186c1ba006e4c Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 17:13:15 +0200 Subject: [PATCH 07/14] EmailJournalAttachment: verarbeitet wenn Anhang gesendet wurde --- SL/Mailer.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/SL/Mailer.pm b/SL/Mailer.pm index a539c1a6c6..deff279b88 100644 --- a/SL/Mailer.pm +++ b/SL/Mailer.pm @@ -230,6 +230,7 @@ sub _create_attachment_part { mime_type => $attributes{content_type}, content => ( $email_journal > 1 ? $attachment_content : ' '), file_id => $file_id, + processed => 1, ); return $ent; From 3ac8bdc7c603aa945cbf1517aa763284a6838554 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 17:22:57 +0200 Subject: [PATCH 08/14] EmailJournal: Nach unverarbeiteten E-Mails filtern --- SL/Controller/EmailJournal.pm | 3 +++ SL/DB/EmailJournal.pm | 6 +++++ SL/DB/Manager/EmailJournal.pm | 25 ++++++++++++++++++- locale/de/all | 2 ++ locale/en/all | 2 ++ .../email_journal/_filter.html | 8 ++++++ .../design40_webpages/email_journal/list.html | 2 ++ templates/webpages/email_journal/_filter.html | 8 ++++++ templates/webpages/email_journal/list.html | 2 ++ 9 files changed, 57 insertions(+), 1 deletion(-) diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index b56fbce4de..402629db65 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -779,6 +779,7 @@ sub init_models { record_type => t8('Record Type'), obsolete => t8('Obsolete'), linked_to => t8('Linked to'), + has_unprocessed_attachments => t8('Has unprocessed attachments'), }, ); } @@ -814,6 +815,8 @@ sub init_filter_summary { push @filter_strings, $::locale->text('Linked') if $filter->{'linked_to:eq_ignore_empty'} eq '1'; push @filter_strings, $::locale->text('Not linked') if $filter->{'linked_to:eq_ignore_empty'} eq '0'; + push @filter_strings, $::locale->text('Unprocessed') if $filter->{'has_unprocessed_attachments:eq_ignore_empty'} eq '1'; + push @filter_strings, $::locale->text('Processed') if $filter->{'has_unprocessed_attachments:eq_ignore_empty'} eq '0'; return join ', ', @filter_strings; } diff --git a/SL/DB/EmailJournal.pm b/SL/DB/EmailJournal.pm index d2ffe5fca0..baa79c7c5c 100644 --- a/SL/DB/EmailJournal.pm +++ b/SL/DB/EmailJournal.pm @@ -57,6 +57,12 @@ sub linked { return !!scalar @{$self->linked_records}; } +sub has_unprocessed_attachments { + my ($self) = @_; + + return scalar grep{$_->processed == 0} @{$self->attachments}; +} + 1; __END__ diff --git a/SL/DB/Manager/EmailJournal.pm b/SL/DB/Manager/EmailJournal.pm index c51d350af5..5873a84a85 100644 --- a/SL/DB/Manager/EmailJournal.pm +++ b/SL/DB/Manager/EmailJournal.pm @@ -33,6 +33,22 @@ __PACKAGE__->add_filter_specs( ) )} => \'TRUE'; }, + has_unprocessed_attachments => sub { + my ($key, $value, $prefix) = @_; + + # if $value is truish, we want at least one link otherwise we want none + my $comp = !!$value ? '>' : '='; + + # table emial_journal is aliased as t1 + return + \qq{( + SELECT CASE WHEN count(*) $comp 0 THEN TRUE ELSE FALSE END + FROM email_journal_attachments + WHERE + email_journal_attachments.email_journal_id = t1.id + AND email_journal_attachments.processed = FALSE + )} => \'TRUE'; + }, ); sub _sort_spec { @@ -51,7 +67,14 @@ sub _sort_spec { record_links.to_table = 'email_journal'::varchar(50) AND record_links.to_id = email_journal.id ) - )} + )}, + has_unprocessed_attachments => qq{( + SELECT count(*) + FROM email_journal_attachments + WHERE + email_journal_attachments.email_journal_id = email_journal.id + AND email_journal_attachments.processed = FALSE + )}, }, ); } diff --git a/locale/de/all b/locale/de/all index 2d1124de07..1300f05775 100755 --- a/locale/de/all +++ b/locale/de/all @@ -1921,6 +1921,7 @@ $ ./scripts/installation_check.pl', 'Hardcopy' => 'Seite drucken', 'Has item type' => 'Hat Regeltypen', 'Has serial number' => 'Hat eine Serienummer', + 'Has unprocessed attachments' => 'Besitzt nicht verarbeitete Anhänge', 'Headers' => 'Kopfzeilen', 'Heading' => 'Überschrift', 'Help Template Variables' => 'Hilfe zu Dokumenten-Variablen', @@ -4664,6 +4665,7 @@ $ ./scripts/installation_check.pl', 'Unknown problem type.' => 'Unbekannter Problem-Typ', 'Unlink bank transactions' => 'Bankverbuchung(en) rückgängig machen', 'Unlock System' => 'System entsperren', + 'Unprocessed' => 'Nicht verarbeitet', 'Unsuccessfully executed:\n' => 'Erfolglos ausgeführt:', 'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)', 'Until' => 'Bis', diff --git a/locale/en/all b/locale/en/all index 114f7cf95c..0e4878aa1f 100755 --- a/locale/en/all +++ b/locale/en/all @@ -1920,6 +1920,7 @@ $self->{texts} = { 'Hardcopy' => '', 'Has item type' => '', 'Has serial number' => '', + 'Has unprocessed attachments' => '', 'Headers' => '', 'Heading' => '', 'Help Template Variables' => '', @@ -4662,6 +4663,7 @@ $self->{texts} = { 'Unknown problem type.' => '', 'Unlink bank transactions' => '', 'Unlock System' => '', + 'Unprocessed' => '', 'Unsuccessfully executed:\n' => '', 'Unsupported image type (supported types: #1)' => '', 'Until' => '', diff --git a/templates/design40_webpages/email_journal/_filter.html b/templates/design40_webpages/email_journal/_filter.html index 7d0a82d6ce..22ec3094bc 100644 --- a/templates/design40_webpages/email_journal/_filter.html +++ b/templates/design40_webpages/email_journal/_filter.html @@ -64,6 +64,14 @@ ) %] + + [% LxERP.t8("Has unprocessed attachments") %] + [% L.yes_no_tag("filter.has_unprocessed_attachments:eq_ignore_empty", + filter.has_unprocessed_attachments, with_empty=1, + default=filter.has_unprocessed_attachments_eq_ignore_empty + ) %] + + [% L.hidden_tag("sort_by", FORM.sort_by) %] diff --git a/templates/design40_webpages/email_journal/list.html b/templates/design40_webpages/email_journal/list.html index d8f8f12295..54bd5eaea0 100644 --- a/templates/design40_webpages/email_journal/list.html +++ b/templates/design40_webpages/email_journal/list.html @@ -28,6 +28,7 @@

[% FORM.title %]

[% L.sortable_table_header("record_type") %] [% L.sortable_table_header("obsolete") %] [% L.sortable_table_header("linked_to") %] + [% L.sortable_table_header("has_unprocessed_attachments") %] @@ -51,6 +52,7 @@

[% FORM.title %]

[% P.record.simple_grouped_record_list(entry.linked_records) %] + [% HTML.escape(entry.has_unprocessed_attachments) %] [% END %] diff --git a/templates/webpages/email_journal/_filter.html b/templates/webpages/email_journal/_filter.html index 702aa41b36..a5c9f14c34 100644 --- a/templates/webpages/email_journal/_filter.html +++ b/templates/webpages/email_journal/_filter.html @@ -66,6 +66,14 @@ ) %] + + [% LxERP.t8("Has unprocessed attachments") %] + [% L.yes_no_tag("filter.has_unprocessed_attachments:eq_ignore_empty", + filter.has_unprocessed_attachments, with_empty=1, + default=filter.has_unprocessed_attachments_eq_ignore_empty + ) %] + + [% L.hidden_tag("sort_by", FORM.sort_by) %] diff --git a/templates/webpages/email_journal/list.html b/templates/webpages/email_journal/list.html index 435525c51d..e2c2e4e28b 100644 --- a/templates/webpages/email_journal/list.html +++ b/templates/webpages/email_journal/list.html @@ -27,6 +27,7 @@

[% FORM.title %]

[% L.sortable_table_header("record_type") %] [% L.sortable_table_header("obsolete") %] [% L.sortable_table_header("linked_to") %] + [% L.sortable_table_header("has_unprocessed_attachments") %] @@ -57,6 +58,7 @@

[% FORM.title %]

[% P.record.simple_grouped_record_list(entry.linked_records) %] + [% HTML.escape(entry.has_unprocessed_attachments) %] [%- END %] From b4a40a3c36d9856ca5fd15a54122634feb90d3fa Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 17:46:37 +0200 Subject: [PATCH 09/14] =?UTF-8?q?EmailJournal:=20Verarbeitet-Status=20von?= =?UTF-8?q?=20Anh=C3=A4ngen=20per=20Button=20=C3=A4ndern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/Controller/EmailJournal.pm | 28 +++++++++++++++++++ js/kivi.EmailJournal.js | 8 ++++++ locale/de/all | 3 ++ locale/en/all | 3 ++ .../design40_webpages/email_journal/show.html | 10 ++++++- templates/webpages/email_journal/show.html | 10 ++++++- 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index 402629db65..6529266986 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -551,6 +551,34 @@ sub action_toggle_obsolete { return; } +sub action_toggle_attachment_processed { + my ($self) = @_; + + $::auth->assert('email_journal'); + + my $attachment = SL::DB::EmailJournalAttachment->new( + id => $::form->{attachment_id} + )->load(); + $self->entry($attachment->email_journal); + + if (!$self->can_view_all && ($self->entry->sender_id != SL::DB::Manager::Employee->current->id)) { + $::form->error(t8('You do not have permission to access this entry.')); + } + + $attachment->processed(!$attachment->processed); + $attachment->save; + + $self->js + ->html('#processed_' . $attachment->id, $attachment->processed_as_bool_yn) + ->flash('info', + $attachment->processed ? + t8('Attachment \'#1\' set to processed.', $attachment->name) + : t8('Attachment \'#1\' set to unprocessed.', $attachment->name) + )->render(); + + return; +} + # # filters # diff --git a/js/kivi.EmailJournal.js b/js/kivi.EmailJournal.js index acd72e672c..82c8116ad7 100644 --- a/js/kivi.EmailJournal.js +++ b/js/kivi.EmailJournal.js @@ -72,4 +72,12 @@ namespace('kivi.EmailJournal', function(ns) { $.post("controller.pl", data, kivi.eval_json_result); } + + ns.toggle_attachment_processed = function(attachment_id) { + let data = $('#record_action_form').serializeArray(); + data.push({ name: 'action', value: 'EmailJournal/toggle_attachment_processed' }); + data.push({ name: 'attachment_id', value: attachment_id }); + + $.post("controller.pl", data, kivi.eval_json_result); + } }); diff --git a/locale/de/all b/locale/de/all index 1300f05775..b80beafdde 100755 --- a/locale/de/all +++ b/locale/de/all @@ -447,6 +447,8 @@ $self->{texts} = { 'Attach PDF:' => 'PDF anhängen', 'Attached Filename' => 'Name des Dateianhangs', 'Attachment' => 'als Anhang', + 'Attachment \'#1\' set to processed.' => 'Anhang \'#1\' auf verarbeitet gesetzt.', + 'Attachment \'#1\' set to unprocessed.' => 'Anhang \'#1\' auf nicht verarbeitet gesetzt.', 'Attachment name' => 'Name des Anhangs', 'Attachments' => 'Dateianhänge', 'Attempt to call an undefined sub named \'%s\'' => 'Es wurde versucht, eine nicht definierte Unterfunktion namens \'%s\' aufzurufen.', @@ -4537,6 +4539,7 @@ $ ./scripts/installation_check.pl', 'To user login' => 'Zum Benutzerlogin', 'Today' => 'heute', 'Toggle marker' => 'Markierung umschalten', + 'Toggle processed' => 'Verarbeitet-Status umschalten', 'Toggle selection' => 'Auswahl umkehren', 'Too many results (#1 from #2).' => 'Zu viele Artikel (#1 von #2)', 'Too much recursions in assembly tree (>100)' => 'Zu tiefe Verschachtelung (>100) des Erzeugnisbaum', diff --git a/locale/en/all b/locale/en/all index 0e4878aa1f..c25b890bb5 100755 --- a/locale/en/all +++ b/locale/en/all @@ -447,6 +447,8 @@ $self->{texts} = { 'Attach PDF:' => '', 'Attached Filename' => '', 'Attachment' => '', + 'Attachment \'#1\' set to processed.' => '', + 'Attachment \'#1\' set to unprocessed.' => '', 'Attachment name' => '', 'Attachments' => '', 'Attempt to call an undefined sub named \'%s\'' => '', @@ -4535,6 +4537,7 @@ $self->{texts} = { 'To user login' => '', 'Today' => '', 'Toggle marker' => '', + 'Toggle processed' => '', 'Toggle selection' => '', 'Too many results (#1 from #2).' => '', 'Too much recursions in assembly tree (>100)' => '', diff --git a/templates/design40_webpages/email_journal/show.html b/templates/design40_webpages/email_journal/show.html index 0f3ad74f92..13a0970d38 100644 --- a/templates/design40_webpages/email_journal/show.html +++ b/templates/design40_webpages/email_journal/show.html @@ -34,6 +34,7 @@

[% FORM.title %]

[% 'MIME type' | $T8 %] [% 'Size' | $T8 %] [% 'Processed' | $T8 %] + [% 'Action' | $T8 %] @@ -42,7 +43,14 @@

[% FORM.title %]

[% L.link(SELF.url_for(action="download_attachment", id=attachment.id), attachment.name) %] [% HTML.escape(attachment.mime_type) %] [% HTML.escape(LxERP.format_amount(attachment.content.length, 0)) %] - [% HTML.escape(attachment.processed_as_bool_yn) %] + + [% HTML.escape(attachment.processed_as_bool_yn) %] + + [% L.button_tag( + "kivi.EmailJournal.toggle_attachment_processed(" _ attachment.id _ ")", + LxERP.t8("Toggle processed") + ) %] + [% END %] diff --git a/templates/webpages/email_journal/show.html b/templates/webpages/email_journal/show.html index 194be00cce..56a0e991e0 100644 --- a/templates/webpages/email_journal/show.html +++ b/templates/webpages/email_journal/show.html @@ -24,6 +24,7 @@

[% LxERP.t8("Attachments") %]

[% LxERP.t8("MIME type") %] [% LxERP.t8("Size") %] [% LxERP.t8("Processed") %] + [% 'Action' | $T8 %] @@ -33,7 +34,14 @@

[% LxERP.t8("Attachments") %]

[% L.link(SELF.url_for(action="download_attachment", id=attachment.id), attachment.name) %] [% HTML.escape(attachment.mime_type) %] [% HTML.escape(LxERP.format_amount(attachment.content.length, 0)) %] - [% HTML.escape(attachment.processed_as_bool_yn) %] + + [% HTML.escape(attachment.processed_as_bool_yn) %] + + [% L.button_tag( + "kivi.EmailJournal.toggle_attachment_processed(" _ attachment.id _ ")", + LxERP.t8("Toggle processed") + ) %] + [% END %] From 31d1d9afa3603fa7dec568942434b82d311c78db Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Mon, 13 May 2024 18:10:30 +0200 Subject: [PATCH 10/14] S:H:EmailProcessing: automatisches Entpacken von Zip-Dateien --- SL/Helper/EmailProcessing.pm | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/SL/Helper/EmailProcessing.pm b/SL/Helper/EmailProcessing.pm index 64431b9947..bab78da026 100644 --- a/SL/Helper/EmailProcessing.pm +++ b/SL/Helper/EmailProcessing.pm @@ -6,6 +6,8 @@ use warnings; use Carp; use XML::LibXML; +use Archive::Zip; +use File::MimeInfo::Magic; use SL::ZUGFeRD; use SL::Locale::String qw(t8); @@ -89,6 +91,43 @@ sub process_attachments_zugferd { return 0; } +sub process_attachments_extract_zip_file { + my ($self, $email_journal, $attachment, %params) = @_; + + my $mime_type = $attachment->mime_type; + if($mime_type eq 'application/octet-stream') { + $mime_type = File::MimeInfo::Magic::mimetype($attachment->name); + } + return unless $mime_type eq 'application/zip'; + + my $zip = Archive::Zip->new; + open my $fh, "+<", \$attachment->content; + $zip->readFromFileHandle($fh); + use Data::Dumper; + use Archive::Zip::MemberRead; + + my @new_attachments; + foreach my $member ($zip->members) { + my $member_fh = Archive::Zip::MemberRead->new($zip, $member); + my $member_content = ''; + while (defined(my $line = $member_fh->getline())) { + $member_content .= $line . "\n"; + } + my $new_attachment = SL::DB::EmailJournalAttachment->new( + name => $member->fileName, + content => $member_content, + mime_type => File::MimeInfo::Magic::mimetype($member->fileName) || 'text/plain', + email_journal_id => $email_journal->id, + )->save; + $email_journal->add_attachments($new_attachment); + push @new_attachments, $new_attachment; + } + $attachment->update_attributes(processed => 1); + + return 0; +} + + sub _add_attachment_to_record { my ($self, $email_journal, $attachment, $record) = @_; From 12cb9c3937750b401a7157a9ea062882e686a0f0 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Sat, 1 Jun 2024 01:51:21 +0200 Subject: [PATCH 11/14] =?UTF-8?q?FIX:=20EmailJournalAttachment:=20verarbei?= =?UTF-8?q?tet=20wenn=20zum=20Beleg=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/DB/EmailJournalAttachment.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SL/DB/EmailJournalAttachment.pm b/SL/DB/EmailJournalAttachment.pm index 92bef09681..3ae8e4b268 100644 --- a/SL/DB/EmailJournalAttachment.pm +++ b/SL/DB/EmailJournalAttachment.pm @@ -61,7 +61,8 @@ sub add_file_to_record { die 'Storing the attachment file to the file management failed: ' . $@; }; } - $self->processed(1)->save; + $self->processed(1); + $self->save; } From a5dff5142cb9e1a4138a6735cf6d652e5bb7e191 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Fri, 11 Oct 2024 14:53:37 +0200 Subject: [PATCH 12/14] =?UTF-8?q?EmailJournal:=20Anh=C3=A4nge=20in=20Beric?= =?UTF-8?q?ht=20anzeigen=20und=20sortieren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/Controller/EmailJournal.pm | 1 + SL/DB/Manager/EmailJournal.pm | 10 ++++++++++ templates/design40_webpages/email_journal/list.html | 6 ++++++ templates/webpages/email_journal/list.html | 6 ++++++ 4 files changed, 23 insertions(+) diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index 6529266986..b4b9a5f6c8 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -801,6 +801,7 @@ sub init_models { from => t8('From'), recipients => t8('Recipients'), subject => t8('Subject'), + attachments => t8('Attachments'), sent_on => t8('Sent on'), status => t8('Status'), extended_status => t8('Extended status'), diff --git a/SL/DB/Manager/EmailJournal.pm b/SL/DB/Manager/EmailJournal.pm index 5873a84a85..057a58427f 100644 --- a/SL/DB/Manager/EmailJournal.pm +++ b/SL/DB/Manager/EmailJournal.pm @@ -68,6 +68,16 @@ sub _sort_spec { AND record_links.to_id = email_journal.id ) )}, + attachment => qq{( + SELECT STRING_AGG( + email_journal_attachments.name, + ', ' + ORDER BY email_journal_attachments.position ASC + ) + FROM email_journal_attachments + WHERE + email_journal_attachments.email_journal_id = email_journal.id + )}, has_unprocessed_attachments => qq{( SELECT count(*) FROM email_journal_attachments diff --git a/templates/design40_webpages/email_journal/list.html b/templates/design40_webpages/email_journal/list.html index 54bd5eaea0..dff920fd9a 100644 --- a/templates/design40_webpages/email_journal/list.html +++ b/templates/design40_webpages/email_journal/list.html @@ -23,6 +23,7 @@

[% FORM.title %]

[% L.sortable_table_header("recipients") %] [% L.sortable_table_header("subject") %] [% L.sortable_table_header("sent_on") %] + [% L.sortable_table_header("attachments") %] [% L.sortable_table_header("status") %] [% L.sortable_table_header("extended_status") %] [% L.sortable_table_header("record_type") %] @@ -45,6 +46,11 @@

[% FORM.title %]

[% HTML.escape(entry.recipients) %] [% HTML.escape(entry.subject) %] [% HTML.escape(entry.sent_on.to_lxoffice('precision' => 'second')) %] + + [% FOREACH attachment = entry.attachments %] + [% HTML.escape(attachment.name) %] + [% END %] + [% P.email_journal.entry_status(entry) %] [% HTML.escape(entry.extended_status) %] [% HTML.escape(RECORD_TYPES_TO_TEXT.${entry.record_type}) %] diff --git a/templates/webpages/email_journal/list.html b/templates/webpages/email_journal/list.html index e2c2e4e28b..5aa7a4bfde 100644 --- a/templates/webpages/email_journal/list.html +++ b/templates/webpages/email_journal/list.html @@ -22,6 +22,7 @@

[% FORM.title %]

[% L.sortable_table_header("recipients") %] [% L.sortable_table_header("subject") %] [% L.sortable_table_header("sent_on") %] + [% L.sortable_table_header("attachments") %] [% L.sortable_table_header("status") %] [% L.sortable_table_header("extended_status") %] [% L.sortable_table_header("record_type") %] @@ -51,6 +52,11 @@

[% FORM.title %]

[%- HTML.escape(entry.recipients) %] [%- HTML.escape(entry.subject) %] [%- HTML.escape(entry.sent_on.to_lxoffice('precision' => 'second')) %] + + [% FOREACH attachment = entry.attachments %] + [% HTML.escape(attachment.name) %] + [% END %] + [% P.email_journal.entry_status(entry) %] [%- HTML.escape(entry.extended_status) %] [% HTML.escape(RECORD_TYPES_TO_TEXT.${entry.record_type}) %] From 6aa91d87a08278d91d6dafc7b5443af6229b1e20 Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Sat, 19 Oct 2024 11:00:34 +0200 Subject: [PATCH 13/14] =?UTF-8?q?S:Presenter:Filter:=20Separaten=20Present?= =?UTF-8?q?er=20f=C3=BCr=20komplexe=20Berichtsfilter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/Controller/Reclamation.pm | 33 +- SL/Presenter/Filter.pm | 347 ++++++++++++++++++ .../Reclamation.pm} | 300 +++------------ 3 files changed, 411 insertions(+), 269 deletions(-) create mode 100644 SL/Presenter/Filter.pm rename SL/Presenter/{ReclamationFilter.pm => Filter/Reclamation.pm} (56%) diff --git a/SL/Controller/Reclamation.pm b/SL/Controller/Reclamation.pm index f28aca5d3a..f922aab11d 100644 --- a/SL/Controller/Reclamation.pm +++ b/SL/Controller/Reclamation.pm @@ -6,7 +6,7 @@ use parent qw(SL::Controller::Base); use SL::Helper::Flash qw(flash_later); use SL::HTML::Util; use SL::Presenter::Tag qw(select_tag hidden_tag div_tag); -use SL::Presenter::ReclamationFilter qw(filter); +use SL::Presenter::Filter::Reclamation; use SL::Locale::String qw(t8); use SL::SessionFile::Random; use SL::PriceSource; @@ -216,10 +216,12 @@ sub action_save { sub action_list { my ($self) = @_; + $::form->{filter} ||= {}; + $self->_setup_search_action_bar; - $self->prepare_report; + my $report = $self->prepare_report; $self->report_generator_list_objects( - report => $self->{report}, + report => $report, objects => $self->models->get, options => { action_bar_additional_submit_values => { @@ -1186,7 +1188,7 @@ sub init_models { closed => t8('Closed'), }, query => [ - SL::DB::Manager::Reclamation->type_filter($self->type), + (record_type => $self->type), (salesman_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales && !$::auth->assert('sales_all_edit', 1)), (employee_id => SL::DB::Manager::Employee->current->id) x ($self->reclamation->is_sales && !$::auth->assert('sales_all_edit', 1)), (employee_id => SL::DB::Manager::Employee->current->id) x (!$self->reclamation->is_sales && !$::auth->assert('purchase_all_edit', 1)), @@ -1672,10 +1674,6 @@ sub prepare_report { my ($self) = @_; my $report = SL::ReportGenerator->new(\%::myconfig, $::form); - $report->{title} = t8('Sales Reclamations'); - if ($self->type eq PURCHASE_RECLAMATION_TYPE()){ - $report->{title} = t8('Purchase Reclamations'); - } $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i; $self->models->add_additional_url_params(type => $self->type); @@ -1683,8 +1681,6 @@ sub prepare_report { my $callback = $self->models->get_callback; - $self->{report} = $report; - # TODO: shipto_id is not linked to custom_shipto my @columns_order = qw( id @@ -1892,10 +1888,13 @@ sub prepare_report { unless ($::form->{active_in_report}) { $::form->{active_in_report}->{$_} = 1 foreach @default_columns; } + $self->models->add_additional_url_params( - active_in_report => $::form->{active_in_report}); - map { $column_defs{$_}->{visible} = $::form->{active_in_report}->{"$_"} } - keys %column_defs; + active_in_report => $::form->{active_in_report} + ); + + $column_defs{$_}->{visible} = $::form->{active_in_report}->{"$_"} || 0 + foreach keys %column_defs; ## add cvars TODO: Add own cvars #my %cvar_column_defs = map { @@ -1915,7 +1914,7 @@ sub prepare_report { # make all sortable my @sortable = keys %column_defs; - my $filter_html = SL::Presenter::ReclamationFilter::filter( + my $filter_html = SL::Presenter::Filter::Reclamation::filter( $::form->{filter}, $self->type, active_in_report => $::form->{active_in_report} ); @@ -1940,9 +1939,11 @@ sub prepare_report { $report->set_columns(%column_defs); $report->set_column_order(@columns_order); #$report->set_export_options(qw(list filter), @cvar_column_form_names); TODO: for cvars - $report->set_export_options(qw(list filter active_in_report)); + $report->set_export_options('list', qw(filter active_in_report)); $report->set_options_from_form; $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable); + + return $report; } sub _setup_edit_action_bar { @@ -2139,7 +2140,7 @@ sub _setup_search_action_bar { $bar->add( action => [ t8('Update'), - submit => [ '#search_form', { action => 'Reclamation/list', type => $self->type } ], + submit => [ '#filter_form', { action => 'Reclamation/list', type => $self->type } ], accesskey => 'enter', ], link => [ diff --git a/SL/Presenter/Filter.pm b/SL/Presenter/Filter.pm new file mode 100644 index 0000000000..2cdf046b54 --- /dev/null +++ b/SL/Presenter/Filter.pm @@ -0,0 +1,347 @@ +package SL::Presenter::Filter; + +use strict; + +use SL::Presenter::EscapedText qw(escape is_escaped); +use SL::Presenter::Tag qw(html_tag input_tag select_tag date_tag checkbox_tag); +use SL::Locale::String qw(t8); + +use Carp; +use List::Util qw(min); +use Params::Validate qw(:all); + +sub create_filter { + validate_pos(@_, + { + type => HASHREF, + default => {}, + callbacks => { + has_all_keys => sub { + foreach my $main_key (keys %{$_[0]}) { + foreach my $sub_key (qw( + position text input_type input_name + )) { + return die "Key '$sub_key' is missing under '$main_key'." + unless exists $_[0]->{$main_key}->{$sub_key}; + } + } + return 1; + } + }, + }, + (0) x (@_ - 1) # allow extra parameters + ); + my $filter_elements = shift @_; + my %params = validate_with( + params => \@_, + spec => { + }, + allow_extra => 1, + ); + + my @filter_element_params = + sort { $a->{position} <=> $b->{position} } + grep { $_->{active} } + values %{$filter_elements}; + + my @filter_elements; + for my $filter_element_param (@filter_element_params) { + + my $filter_element = _create_input_element($filter_element_param, %params); + + push @filter_elements, $filter_element; + } + + my $filter_form_div = _create_filter_form(\@filter_elements, %params); + + is_escaped($filter_form_div); +} + +sub _create_input_element { + my $element_param = shift @_; + my %params = validate_with( + params => \@_, + spec => { + no_show => { + type => BOOLEAN, + default => 0 + }, + active_in_report => { + type => HASHREF, + default => {} + }, + }, + allow_extra => 1, + ); + + my $element_th = html_tag('th', $element_param->{text}, align => 'right'); + + my $element_input = ''; + + if($element_param->{input_type} eq 'input_tag') { + + $element_input = input_tag($element_param->{input_name}, $element_param->{input_default}); + + } elsif ($element_param->{input_type} eq 'select_tag') { + + $element_input = select_tag($element_param->{input_name}, $element_param->{input_values}, default => $element_param->{input_default}) + + } elsif ($element_param->{input_type} eq 'yes_no_tag') { + + $element_input = select_tag($element_param->{input_name}, [ [ 1 => t8('Yes') ], [ 0 => t8('No') ] ], default => $element_param->{input_default}, with_empty => 1) + + } elsif($element_param->{input_type} eq 'date_tag') { + + my $after_input = + html_tag('th', t8("After"), align => 'right') . + html_tag('td', + date_tag("filter." . $element_param->{input_name} . ":date::ge", $element_param->{input_default_ge}) + ) + ; + my $before_input = + html_tag('th', t8("Before"), align => 'right') . + html_tag('td', + date_tag("filter." . $element_param->{input_name} . ":date::le", $element_param->{input_default_le}) + ) + ; + + $element_input = + html_tag('table', + html_tag('tr', $after_input) + . + html_tag('tr', $before_input) + ) + ; + } else { + confess "unknown input_type " . $element_param->{input_type}; + } + + my $element_input_td = html_tag('td', + $element_input, + nowrap => 1, + ); + + my $element_checkbox_td = ''; + unless($params{no_show} || $element_param->{report_id} eq '') { + my $checkbox = checkbox_tag( + 'active_in_report.' . $element_param->{report_id}, + checked => $params{active_in_report}->{$element_param->{report_id}}, + for_submit => 1 + ); + $element_checkbox_td = html_tag('td', $checkbox); + } + + return $element_th . $element_input_td . $element_checkbox_td; +} + +sub _create_filter_form { + my $ref_elements = shift @_; + my %params = validate_with( + params => \@_, + spec => { + }, + allow_extra => 1, + ); + + my $filter_table = _create_input_div($ref_elements, %params); + + my $filter_form = html_tag('form', $filter_table, method => 'post', action => 'controller.pl', id => 'filter_form'); + + return $filter_form; +} + +sub _create_input_div { + my $ref_elements = shift @_; + my %params = validate_with( + params => \@_, + spec => { + count_columns => { + type => SCALAR, + default => 4, + }, + no_show => { + type => BOOLEAN, + default => 0, + }, + }, + allow_extra => 1, + ); + my @elements = @{$ref_elements}; + + my $div_columns = ""; + + my $elements_per_column = (int((scalar(@{$ref_elements}) - 1) / $params{count_columns}) + 1); + for my $i (0 .. (min(scalar @elements, $params{count_columns}) - 1)) { + + my $rows = ""; + for my $j (0 .. ($elements_per_column - 1) ) { + my $idx = $elements_per_column * $i + $j; + my $element = $elements[$idx]; + $rows .= html_tag('tr', $element); + + } + $div_columns .= html_tag('div', + html_tag('table', + html_tag('tr', + html_tag('td') + . html_tag('th', t8('Filter')) + . ( $params{no_show} ? '' : html_tag('th', t8('Show')) ) + ) + . $rows + ), + style => "flex:1"); + } + + my $input_div = html_tag('div', $div_columns, style => "display:flex;flex-wrap:wrap"); + + return $input_div; +} + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::Presenter::Filter - Presenter module for a generic Filter. + +=head1 SYNOPSIS + + my $filter_elements = { + id => { + 'position' => 1, + 'text' => t8("ID"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.id:number', + 'input_default' => $::form->{filter}->{'id:number'}, + 'report_id' => 'id', + 'active' => 1, + }, + # ... + }; + + my $filter_html = SL::Presenter::Filter::create_filter( + $filter_elements, + active_in_report => ['id'], + ); + + +=head1 FUNCTIONS + +=over 4 + +=item C + +Returns a rendered version (actually an instance of +L) of a filter form for reclamations of type +C<$reclamation_type>. + +C<$filter_elements> is a hash reference with the values declaring which inputs +to create. + +=over 2 + +FILTER ELEMENTS + +A filter element is a hash reference. Each filter has a unique key and can have +entries for: + +=over 4 + +=item * position (mandatory) + +Is a number after which the elements are ordered. This can be a float. + +=item * text (mandatory) + +Is shown before the input field. + +=item * input_name (mandatory) + +C is used to set the name of the field, which should match the +filter syntax. + +=item * C (mandatory) + +This must be C, C, C or C. It sets +the input type for the filter. + +=over 2 + +=item * C + +Creates a text input field. The default value of this field is set to the +C entry of the filter element. + +=item * C + +Creates a drop down field. C is used to set the options. +See L for more details. The default value +of this field is set to the C entry. + +=item * C + +Creates a yes/no input field. The default value of this field is set to the +C entry. + +=item * C + +Creates two date input fields. One filters for after the date and the other +filters for before the date. The default values of these fields are set to the +C and C entries of the filter element. +For the first field ":date::ge" and for the second ":date::le" is added to the +end of C. + +=back + +=item * C + +Is used to generate the id of the check box after the input field. The value of +the check box can be found in the form under +C<$::form-E{'active_in_report'}-E{report_id}>. + +=item * C + +If falsish the element is ignored. + +=item * C, C, C + +Look at C to see how they are used. + +=back + +=back + +C<%params> can include: + +=over 2 + +=item * no_show + +If falsish (the default) then a check box is added after the input field. Which +specifies whether the corresponding column appears in the report. The value of +the check box can be changed by the user. + +=item * active_in_report + +If C<$params{no_show}> is falsish, this is used to set the values of the check +boxes, after the input fields. This should be set to the C +value of the last C<$::form>. + +=back + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Tamino Steinert Etamino.steinert@tamino.stE + +=cut diff --git a/SL/Presenter/ReclamationFilter.pm b/SL/Presenter/Filter/Reclamation.pm similarity index 56% rename from SL/Presenter/ReclamationFilter.pm rename to SL/Presenter/Filter/Reclamation.pm index a7f0dc3548..1b963d4aa9 100644 --- a/SL/Presenter/ReclamationFilter.pm +++ b/SL/Presenter/Filter/Reclamation.pm @@ -1,22 +1,16 @@ -package SL::Presenter::ReclamationFilter; +package SL::Presenter::Filter::Reclamation; + +use parent SL::Presenter::Filter; use strict; -use SL::Presenter::EscapedText qw(escape is_escaped); -use SL::Presenter::Tag qw(html_tag input_tag select_tag date_tag checkbox_tag); use SL::Locale::String qw(t8); -use Exporter qw(import); -our @EXPORT_OK = qw( -filter -); - -use Carp; +use Params::Validate qw(:all); -sub filter { - my ($filter, $reclamation_type, %params) = @_; +sub get_default_filter_elements { + my ($filter, $reclamation_type) = @_; - $filter ||= undef; #filter should not be '' (empty string); my %default_filter_elements = ( # {{{ 'reason_names' => { 'position' => 1, @@ -31,7 +25,7 @@ sub filter { 'text' => t8("Reclamation ID"), 'input_type' => 'input_tag', 'input_name' => 'filter.id:number', - 'input_default' =>$filter->{'id:number'}, + 'input_default' => $filter->{'id:number'}, 'report_id' => 'id', 'active' => 0, }, @@ -40,7 +34,7 @@ sub filter { 'text' => t8("Reclamation Number"), 'input_type' => 'input_tag', 'input_name' => 'filter.record_number:substr::ilike', - 'input_default' =>$filter->{'record_number:substr::ilike'}, + 'input_default' => $filter->{'record_number:substr::ilike'}, 'report_id' => 'record_number', 'active' => 1, }, @@ -49,7 +43,7 @@ sub filter { 'text' => t8("Employee Name"), 'input_type' => 'input_tag', 'input_name' => 'filter.employee.name:substr::ilike', - 'input_default' =>$filter->{employee}->{'name:substr::ilike'}, + 'input_default' => $filter->{employee}->{'name:substr::ilike'}, 'report_id' => 'employee', 'active' => 1, }, @@ -58,7 +52,7 @@ sub filter { 'text' => t8("Salesman Name"), 'input_type' => 'input_tag', 'input_name' => 'filter.salesman.name:substr::ilike', - 'input_default' =>$filter->{salesman}->{'name:substr::ilike'}, + 'input_default' => $filter->{salesman}->{'name:substr::ilike'}, 'report_id' => 'salesman', 'active' => 1, }, @@ -102,7 +96,7 @@ sub filter { 'text' => t8("Contact Name"), 'input_type' => 'input_tag', 'input_name' => 'filter.contact.cp_name:substr::ilike', - 'input_default' =>$filter->{contact}->{'cp_name:substr::ilike'}, + 'input_default' => $filter->{contact}->{'cp_name:substr::ilike'}, 'report_id' => 'contact', 'active' => 1, }, @@ -111,7 +105,7 @@ sub filter { 'text' => t8("Language Code"), 'input_type' => 'input_tag', 'input_name' => 'filter.language.article_code:substr::ilike', - 'input_default' =>$filter->{language}->{'article_code:substr::ilike'}, + 'input_default' => $filter->{language}->{'article_code:substr::ilike'}, 'report_id' => 'language', 'active' => 1, }, @@ -120,7 +114,7 @@ sub filter { 'text' => t8("Department Description"), 'input_type' => 'input_tag', 'input_name' => 'filter.department.description:substr::ilike', - 'input_default' =>$filter->{department}->{'description:substr::ilike'}, + 'input_default' => $filter->{department}->{'description:substr::ilike'}, 'report_id' => 'department', 'active' => 1, }, @@ -129,7 +123,7 @@ sub filter { 'text' => t8("Project Number"), 'input_type' => 'input_tag', 'input_name' => 'filter.globalproject.projectnumber:substr::ilike', - 'input_default' =>$filter->{globalproject}->{'projectnumber:substr::ilike'}, + 'input_default' => $filter->{globalproject}->{'projectnumber:substr::ilike'}, 'report_id' => 'globalproject', 'active' => 1, }, @@ -138,7 +132,7 @@ sub filter { 'text' => t8("Project Description"), 'input_type' => 'input_tag', 'input_name' => 'filter.globalproject.description:substr::ilike', - 'input_default' =>$filter->{globalproject}->{'description:substr::ilike'}, + 'input_default' => $filter->{globalproject}->{'description:substr::ilike'}, 'active' => 1, }, 'cv_record_number' => { @@ -158,7 +152,7 @@ sub filter { 'text' => t8("Description"), 'input_type' => 'input_tag', 'input_name' => 'filter.transaction_description:substr::ilike', - 'input_default' =>$filter->{'transaction_description:substr::ilike'}, + 'input_default' => $filter->{'transaction_description:substr::ilike'}, 'report_id' => 'transaction_description', 'active' => 1, }, @@ -167,7 +161,7 @@ sub filter { 'text' => t8("Notes"), 'input_type' => 'input_tag', 'input_name' => 'filter.notes:substr::ilike', - 'input_default' =>$filter->{'notes:substr::ilike'}, + 'input_default' => $filter->{'notes:substr::ilike'}, 'report_id' => 'notes', 'active' => 1, }, @@ -176,7 +170,7 @@ sub filter { 'text' => t8("Internal Notes"), 'input_type' => 'input_tag', 'input_name' => 'filter.intnotes:substr::ilike', - 'input_default' =>$filter->{'intnotes:substr::ilike'}, + 'input_default' => $filter->{'intnotes:substr::ilike'}, 'report_id' => 'intnotes', 'active' => 1, }, @@ -185,7 +179,7 @@ sub filter { 'text' => t8("Shipping Point"), 'input_type' => 'input_tag', 'input_name' => 'filter.shippingpoint:substr::ilike', - 'input_default' =>$filter->{'shippingpoint:substr::ilike'}, + 'input_default' => $filter->{'shippingpoint:substr::ilike'}, 'report_id' => 'shippingpoint', 'active' => 1, }, @@ -194,7 +188,7 @@ sub filter { 'text' => t8("Ship via"), 'input_type' => 'input_tag', 'input_name' => 'filter.shipvia:substr::ilike', - 'input_default' =>$filter->{'shipvia:substr::ilike'}, + 'input_default' => $filter->{'shipvia:substr::ilike'}, 'report_id' => 'shipvia', 'active' => 1, }, @@ -203,7 +197,7 @@ sub filter { 'text' => t8("Total"), 'input_type' => 'input_tag', 'input_name' => 'filter.amount:number', - 'input_default' =>$filter->{'amount:number'}, + 'input_default' => $filter->{'amount:number'}, 'report_id' => 'amount', 'active' => 1, }, @@ -212,7 +206,7 @@ sub filter { 'text' => t8("Subtotal"), 'input_type' => 'input_tag', 'input_name' => 'filter.netamount:number', - 'input_default' =>$filter->{'netamount:number'}, + 'input_default' => $filter->{'netamount:number'}, 'report_id' => 'netamount', 'active' => 1, }, @@ -221,7 +215,7 @@ sub filter { 'text' => t8("Delivery Terms"), 'input_type' => 'input_tag', 'input_name' => 'filter.delivery_term.description:substr::ilike', - 'input_default' =>$filter->{delivery_term}->{'description:substr::ilike'}, + 'input_default' => $filter->{delivery_term}->{'description:substr::ilike'}, 'report_id' => 'delivery_term', 'active' => 1, }, @@ -230,7 +224,7 @@ sub filter { 'text' => t8("Payment Terms"), 'input_type' => 'input_tag', 'input_name' => 'filter.payment.description:substr::ilike', - 'input_default' =>$filter->{payment}->{'description:substr::ilike'}, + 'input_default' => $filter->{payment}->{'description:substr::ilike'}, 'report_id' => 'payment', 'active' => 1, }, @@ -239,7 +233,7 @@ sub filter { 'text' => t8("Currency"), 'input_type' => 'input_tag', 'input_name' => 'filter.currency.name:substr::ilike', - 'input_default' =>$filter->{currency}->{'name:substr::ilike'}, + 'input_default' => $filter->{currency}->{'name:substr::ilike'}, 'report_id' => 'currency', 'active' => 1, }, @@ -248,7 +242,7 @@ sub filter { 'text' => t8("Exchangerate"), 'input_type' => 'input_tag', 'input_name' => 'filter.exchangerate:number', - 'input_default' =>$filter->{'exchangerate:number'}, + 'input_default' => $filter->{'exchangerate:number'}, 'report_id' => 'exchangerate', 'active' => 1, }, @@ -257,7 +251,7 @@ sub filter { 'text' => t8("Tax Included"), 'input_type' => 'yes_no_tag', 'input_name' => 'filter.taxincluded', - 'input_default' =>$filter->{taxincluded}, + 'input_default' => $filter->{taxincluded}, 'report_id' => 'taxincluded', 'active' => 1, }, @@ -266,7 +260,7 @@ sub filter { 'text' => t8("Tax zone"), 'input_type' => 'input_tag', 'input_name' => 'filter.taxzone.description:substr::ilike', - 'input_default' =>$filter->{taxzone}->{'description:substr::ilike'}, + 'input_default' => $filter->{taxzone}->{'description:substr::ilike'}, 'report_id' => 'taxzone', 'active' => 1, }, @@ -325,7 +319,7 @@ sub filter { 'text' => t8("Delivered"), 'input_type' => 'yes_no_tag', 'input_name' => 'filter.delivered', - 'input_default' =>$filter->{delivered}, + 'input_default' => $filter->{delivered}, 'report_id' => 'delivered', 'active' => 1, }, @@ -334,142 +328,30 @@ sub filter { 'text' => t8("Closed"), 'input_type' => 'yes_no_tag', 'input_name' => 'filter.closed', - 'input_default' =>$filter->{closed}, + 'input_default' => $filter->{closed}, 'report_id' => 'closed', 'active' => 1, }, ); # }}} - - # combine default and param values for filter_element, - # only replace the lowest occurrence - my %filter_elements = %default_filter_elements; - while(my ($key, $value) = each (%{$params{filter_elements}})) { - if(exists $filter_elements{$key}) { - $filter_elements{$key} = ({ - %{$filter_elements{$key}}, - %{$value}, - }); - } else { - $filter_elements{$key} = $value; - } - } - - my @filter_element_params = - sort { $a->{position} <=> $b->{position} } - grep { $_->{active} } - values %filter_elements; - - my @filter_elements; - for my $filter_element_param (@filter_element_params) { - unless($filter_element_param->{active}) { - next; - } - - my $filter_element = _create_input_element($filter_element_param, %params); - - push @filter_elements, $filter_element; - } - - my $filter_form_div = _create_filter_form(\@filter_elements, %params); - - is_escaped($filter_form_div); + return \%default_filter_elements; } -sub _create_input_element { - my ($element_param, %params) = @_; - - my $element_th = html_tag('th', $element_param->{text}, align => 'right'); - - my $element_input = ''; - - if($element_param->{input_type} eq 'input_tag') { - - $element_input = input_tag($element_param->{input_name}, $element_param->{input_default}); - - } elsif ($element_param->{input_type} eq 'yes_no_tag') { - - $element_input = select_tag($element_param->{input_name}, [ [ 1 => t8('Yes') ], [ 0 => t8('No') ] ], default => $element_param->{input_default}, with_empty => 1) - - } elsif($element_param->{input_type} eq 'date_tag') { - - my $after_input = - html_tag('th', t8("After"), align => 'right') . - html_tag('td', - date_tag("filter." . $element_param->{input_name} . ":date::ge", $element_param->{input_default_ge}) - ) - ; - my $before_input = - html_tag('th', t8("Before"), align => 'right') . - html_tag('td', - date_tag("filter." . $element_param->{input_name} . ":date::le", $element_param->{input_default_le}) - ) - ; - - $element_input = - html_tag('table', - html_tag('tr', $after_input) - . - html_tag('tr', $before_input) - ) - ; - } - - my $element_input_td = html_tag('td', - $element_input, - nowrap => 1, +sub filter { + my $filter = shift @_; + die "filter has to be a hash ref" if ref $filter ne 'HASH'; + my $reclamation_type = shift @_; + my %params = validate_with( + params => \@_, + spec => { + }, + allow_extra => 1, ); - my $element_checkbox_td = ''; - unless($params{no_show} || $element_param->{report_id} eq '') { - my $checkbox = checkbox_tag('active_in_report.' . $element_param->{report_id}, checked => $params{active_in_report}->{$element_param->{report_id}}, for_submit => 1); - $element_checkbox_td = html_tag('td', $checkbox); - } - - return $element_th . $element_input_td . $element_checkbox_td; -} - -sub _create_filter_form { - my ($ref_elements, %params) = @_; - - my $filter_table = _create_input_div($ref_elements, %params); - - my $filter_form = html_tag('form', $filter_table, method => 'post', action => 'controller.pl', id => 'search_form'); - - return $filter_form; -} - -sub _create_input_div { - my ($ref_elements, %params) = @_; - my @elements = @{$ref_elements}; - - my $div_columns = ""; - - $params{count_columns} ||= 4; - my $elements_per_column = (int((scalar(@{$ref_elements}) - 1) / $params{count_columns}) + 1); - for my $i (0 .. ($params{count_columns} - 1)) { - - my $rows = ""; - for my $j (0 .. ($elements_per_column - 1) ) { - my $idx = $elements_per_column * $i + $j; - my $element = $elements[$idx]; - $rows .= html_tag('tr', $element); - - } - $div_columns .= html_tag('div', - html_tag('table', - html_tag('tr', - html_tag('td') - . html_tag('th', t8('Filter')) - . ( $params{no_show} ? '' : html_tag('th', t8('Show')) ) - ) - . $rows - ), - style => "flex:1"); - } - - my $input_div = html_tag('div', $div_columns, style => "display:flex;flex-wrap:wrap"); + # combine default and param values for filter_element, + # only replace the lowest occurrence + my $filter_elements = get_default_filter_elements($filter, $reclamation_type); - return $input_div; + return SL::Presenter::Filter::create_filter($filter_elements, %params); } 1; @@ -482,13 +364,13 @@ __END__ =head1 NAME -SL::Presenter::ReclamationFilter - Presenter module for a generic Filter on +SL::Presenter::Filter::Reclamation - Presenter module for a generic Filter on Reclamation. =head1 SYNOPSIS # in Reclamation Controller - my $filter_html = SL::Presenter::ReclamationFilter::filter( + my $filter_html = SL::Presenter::Filter::Reclamation::filter( $::form->{filter}, $self->type, active_in_report => $::form->{active_in_report} ); @@ -506,95 +388,7 @@ C<$reclamation_type>. C<$filter> should be the C value of the last C<$::form>. This is used to get the previous values of the input fields. -C<%params> can include: - -=over 2 - -=item * no_show - -If falsish (the default) then a check box is added after the input field. Which -specifies whether the corresponding column appears in the report. The value of -the check box can be changed by the user. - -=item * active_in_report - -If C<$params{no_show}> is falsish, this is used to set the values of the check -boxes, after the input fields. This can be set to the C value -of the last C<$::form>. - -=item * filter_elements - -Is combined with the default filter elements. This can be used to override -default values of the filter elements or to add a new ones. - - #deactivate the id and record_number fields - $params{filter_elements} = ({ - id => {active => 0}, - record_number => {active => 0} - }); - -=back - -=back - -=head1 FILTER ELEMENTS - -A filter element is stored in and as a hash map. Each filter has a unique key -and should have entries for: - -=over 4 - -=item * position - -Is a number after which the elements are ordered. This can be a float. - -=item * text - -Is shown before the input field. - -=item * input_type - -This must be C, C or C. It sets the input type -for the filter. - -=over 2 - -=item * input_tag - -Creates a text input field. The default value of this field is set to the -C entry of the filter element. C is used to set the -name of the field, which should match the filter syntax. - -=item * yes_no_tag - -Creates a yes/no input field. The default value of this field is set to the -C entry of the filter element. C is used to set the -name of the field, which should match the filter syntax. - -=item * date_tag - -Creates two date input fields. One filters for after the date and the other -filters for before the date. The default values of these fields are set to the -C and C entries of the filter element. -C is used to set the names of these fields, which should match the -filter syntax. For the first field ":date::ge" and for the second ":date::le" is -added to the end of C. - -=back - -=item * report_id - -Is used to generate the id of the check box after the input field. The value of -the check box can be found in the form under -C<$::form-E{'active_in_report'}-E{report_id}>. - -=item * active - -If falsish the element is ignored. - -=item * input_name, input_default, input_default_ge, input_default_le - -Look at I to see how they are used. +C<%params> fields get forwarded to C. =back From dfdfb88c555d5b4f0be25b551e4bc2aa709b111b Mon Sep 17 00:00:00 2001 From: Tamino Steinert Date: Sat, 19 Oct 2024 15:08:08 +0200 Subject: [PATCH 14/14] EmailJournal: Komplexer Bericht mit komplexen Filter --- SL/Controller/EmailJournal.pm | 197 +++++++++++++- SL/DB/Manager/EmailJournal.pm | 23 +- SL/Presenter/Filter/EmailJournal.pm | 248 ++++++++++++++++++ locale/de/all | 2 +- locale/en/all | 2 +- .../email_journal/_filter.html | 93 ------- .../email_journal/_report_bottom.html | 2 + .../email_journal/_report_top.html | 10 + .../design40_webpages/email_journal/list.html | 69 ----- templates/webpages/email_journal/_filter.html | 87 ------ .../email_journal/_report_bottom.html | 2 + .../webpages/email_journal/_report_top.html | 3 + templates/webpages/email_journal/list.html | 74 ------ 13 files changed, 472 insertions(+), 340 deletions(-) create mode 100644 SL/Presenter/Filter/EmailJournal.pm delete mode 100644 templates/design40_webpages/email_journal/_filter.html create mode 100644 templates/design40_webpages/email_journal/_report_bottom.html create mode 100644 templates/design40_webpages/email_journal/_report_top.html delete mode 100644 templates/design40_webpages/email_journal/list.html delete mode 100644 templates/webpages/email_journal/_filter.html create mode 100644 templates/webpages/email_journal/_report_bottom.html create mode 100644 templates/webpages/email_journal/_report_top.html delete mode 100644 templates/webpages/email_journal/list.html diff --git a/SL/Controller/EmailJournal.pm b/SL/Controller/EmailJournal.pm index b4b9a5f6c8..84c49a1293 100644 --- a/SL/Controller/EmailJournal.pm +++ b/SL/Controller/EmailJournal.pm @@ -4,6 +4,8 @@ use strict; use parent qw(SL::Controller::Base); +use SL::ReportGenerator; +use SL::Controller::Helper::ReportGenerator; use SL::ZUGFeRD; use SL::Controller::ZUGFeRD; use SL::Controller::Helper::GetModels; @@ -11,6 +13,7 @@ use SL::DB::Employee; use SL::DB::EmailJournal; use SL::DB::EmailJournalAttachment; use SL::Presenter::EmailJournal; +use SL::Presenter::Filter::EmailJournal; use SL::Presenter::Record qw(grouped_record_list); use SL::Presenter::Tag qw(html_tag div_tag button_tag); use SL::Helper::Flash; @@ -222,22 +225,21 @@ sub action_list { my ($self) = @_; $::auth->assert('email_journal'); - # default filter - $::form->{filter} ||= {"obsolete:eq_ignore_empty" => 0}; if ( $::instance_conf->get_email_journal == 0 ) { - flash('info', $::locale->text('Storing the emails in the journal is currently disabled in the client configuration.')); + flash('info', t8('Storing the emails in the journal is currently disabled in the client configuration.')); } + + $::form->{filter} ||= { + 'obsolete:eq_ignore_empty' => 0, + }; + $self->setup_list_action_bar; - my @record_types_with_info = $self->get_record_types_with_info(); - my %record_types_to_text = $self->get_record_types_to_text(); - $self->render('email_journal/list', - title => $::locale->text('Email journal'), - ENTRIES => $self->models->get, - MODELS => $self->models, - RECORD_TYPES_WITH_INFO => \@record_types_with_info, - RECORD_TYPES_TO_TEXT => \%record_types_to_text, - ); + my $report = $self->prepare_report; + $self->report_generator_list_objects( + report => $report, + objects => $self->models->get, + ); } sub action_show { @@ -778,6 +780,168 @@ sub find_customer_vendor_from_email { return $customer_vendor; } +sub prepare_report { + my ($self) = @_; + + my %record_types_to_text = $self->get_record_types_to_text(); + my @record_types_with_info = $self->get_record_types_with_info(); + + my $report = SL::ReportGenerator->new(\%::myconfig, $::form); + + my $callback = $self->models->get_callback; + + my @columns_order = qw( + id + sender + from + recipients + subject + sent_on + attachment_names + has_unprocessed_attachments + unprocessed_attachment_names + status + extended_status + record_type + linked_to + obsolete + ); + + my @default_columns = qw( + from + recipients + subject + sent_on + ); + + my %column_defs = ( + id => { + obj_link => sub {$self->url_for( + action => 'show', id => $_[0]->id, callback => $callback + )}, + sub => sub { $_[0]->id }, + }, + sender => { + sub => sub { $_[0]->sender ? $_[0]->sender->name : '' }, + }, + from => { + obj_link => sub {$self->url_for( + action => 'show', id => $_[0]->id, callback => $callback + )}, + sub => sub { $_[0]->from }, + }, + recipients => { + obj_link => sub {$self->url_for( + action => 'show', id => $_[0]->id, callback => $callback + )}, + sub => sub { $_[0]->recipients }, + }, + subject => { + obj_link => sub {$self->url_for( + action => 'show', id => $_[0]->id, callback => $callback + )}, + sub => sub { $_[0]->subject }, + }, + sent_on => { + obj_link => sub {$self->url_for( + action => 'show', id => $_[0]->id, callback => $callback + )}, + sub => sub { $_[0]->sent_on->to_kivitendo(precision => 'minute') }, + }, + attachment_names => { + sub => sub {join(', ', + map {$_->name} + sort {$a->position <=> $b->position} + @{$_[0]->attachments} + )}, + }, + has_unprocessed_attachments => { + sub => sub { $_[0]->has_unprocessed_attachments } + }, + unprocessed_attachment_names => { + sub => sub {join(', ', + map {$_->name} + sort {$a->position <=> $b->position} + grep {$_->processed == 0} + @{$_[0]->attachments} + )}, + }, + status => { + sub => sub { SL::Presenter::EmailJournal::entry_status($_[0]) }, + }, + extended_status => { + sub => sub { $_[0]->extended_status }, + }, + record_type => { + sub => sub { $record_types_to_text{$_[0]->record_type} }, + }, + linked_to => { + raw_data => sub { + SL::Presenter::Record::simple_grouped_record_list($_[0]->linked_records) + } + }, + obsolete => { + sub => sub { $_[0]->obsolete_as_bool_yn } + }, + ); + $column_defs{$_}->{text} ||= + t8( $self->models->get_sort_spec->{$_}->{title} || $_ ) + for keys %column_defs; + + # make all sortable + my @sortable = keys %column_defs; + + unless ($::form->{active_in_report}) { + $::form->{active_in_report}->{$_} = 1 foreach @default_columns; + } + + $column_defs{$_}->{visible} = $::form->{active_in_report}->{"$_"} || 0 + foreach keys %column_defs; + + my $filter_html = SL::Presenter::Filter::EmailJournal::filter( + $::form->{filter}, + active_in_report => $::form->{active_in_report}, + record_types_with_info => \@record_types_with_info, + ); + + $self->models->disable_plugin('paginated') + if $report->{options}{output_format} =~ /^(pdf|csv)$/i; + $self->models->add_additional_url_params( + active_in_report => $::form->{active_in_report} + ); + $self->models->finalize; # for filter laundering + + $report->set_options( + std_column_visibility => 1, + controller_class => 'EmailJournal', + output_format => 'HTML', + raw_top_info_text => $self->render( + 'email_journal/_report_top', + { output => 0 }, + FILTER_HTML => $filter_html, + ), + raw_bottom_info_text => $self->render( + 'email_journal/_report_bottom', + { output => 0 }, + models => $self->models + ), + title => t8('Email journal'), + allow_pdf_export => 1, + allow_csv_export => 1, + ); + $report->set_columns(%column_defs); + $report->set_column_order(@columns_order); + $report->set_export_options('list', qw(filter active_in_report)); + $report->set_options_from_form; + + $self->models->set_report_generator_sort_options( + report => $report, + sortable_columns => \@sortable + ); + + return $report; +} + sub add_js { $::request->{layout}->use_javascript("${_}.js") for qw( kivi.EmailJournal @@ -797,18 +961,23 @@ sub init_models { query => \@where, with_objects => [ 'sender' ], sorted => { + _default => { + by => 'sent_on', + dir => 0, + }, sender => t8('Sender'), from => t8('From'), recipients => t8('Recipients'), subject => t8('Subject'), - attachments => t8('Attachments'), + attachment_names => t8('Attachments'), + has_unprocessed_attachments => t8('Has unprocessed attachments'), + unprocessed_attachment_names => t8('Unprocessed Attachments'), sent_on => t8('Sent on'), status => t8('Status'), extended_status => t8('Extended status'), record_type => t8('Record Type'), obsolete => t8('Obsolete'), linked_to => t8('Linked to'), - has_unprocessed_attachments => t8('Has unprocessed attachments'), }, ); } diff --git a/SL/DB/Manager/EmailJournal.pm b/SL/DB/Manager/EmailJournal.pm index 057a58427f..8d7676eb5c 100644 --- a/SL/DB/Manager/EmailJournal.pm +++ b/SL/DB/Manager/EmailJournal.pm @@ -33,6 +33,16 @@ __PACKAGE__->add_filter_specs( ) )} => \'TRUE'; }, + unprocessed_attachment_names => sub { + my ($key, $value, $prefix) = @_; + return ( + and => [ + 'attachments.name' => $value, + 'attachments.processed' => 0, + ], + 'attachments' + ) + }, has_unprocessed_attachments => sub { my ($key, $value, $prefix) = @_; @@ -68,7 +78,17 @@ sub _sort_spec { AND record_links.to_id = email_journal.id ) )}, - attachment => qq{( + attachment_names => qq{( + SELECT STRING_AGG( + email_journal_attachments.name, + ', ' + ORDER BY email_journal_attachments.position ASC + ) + FROM email_journal_attachments + WHERE + email_journal_attachments.email_journal_id = email_journal.id + )}, + unprocessed_attachment_names => qq{( SELECT STRING_AGG( email_journal_attachments.name, ', ' @@ -77,6 +97,7 @@ sub _sort_spec { FROM email_journal_attachments WHERE email_journal_attachments.email_journal_id = email_journal.id + AND email_journal_attachments.processed = FALSE )}, has_unprocessed_attachments => qq{( SELECT count(*) diff --git a/SL/Presenter/Filter/EmailJournal.pm b/SL/Presenter/Filter/EmailJournal.pm new file mode 100644 index 0000000000..87daf25549 --- /dev/null +++ b/SL/Presenter/Filter/EmailJournal.pm @@ -0,0 +1,248 @@ +package SL::Presenter::Filter::EmailJournal; + +use parent SL::Presenter::Filter; + +use strict; + +use SL::Locale::String qw(t8); + +use Params::Validate qw(:all); + +sub get_default_filter_elements { + my $filter = shift @_; + my %params = validate_with( + params => \@_, + spec => { + record_types_with_info => { + type => ARRAYREF + }, + }, + ); + + my %default_filter_elements = ( # {{{ + id => { + 'position' => 1, + 'text' => t8("ID"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.id:number', + 'input_default' => $filter->{'id:number'}, + 'report_id' => 'id', + 'active' => 0, + }, + sender => { + 'position' => 2, + 'text' => t8("Sender"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.sender.name:substr::ilike', + 'input_default' => $filter->{sender}->{'name:substr::ilike'}, + 'report_id' => 'sender', + 'active' => $::auth->assert('email_employee_readall', 1), + }, + from => { + 'position' => 3, + 'text' => t8("From"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.from:substr::ilike', + 'input_default' => $filter->{'from:substr::ilike'}, + 'report_id' => 'from', + 'active' => 1, + }, + recipients => { + 'position' => 4, + 'text' => t8("Recipients"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.recipients:substr::ilike', + 'input_default' => $filter->{'recipients:substr::ilike'}, + 'report_id' => 'recipients', + 'active' => 1, + }, + subject => { + 'position' => 5, + 'text' => t8("Subject"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.subject:substr::ilike', + 'input_default' => $filter->{'subject:substr::ilike'}, + 'report_id' => 'subject', + 'active' => 1, + }, + sent_on => { + 'position' => 6, + 'text' => t8("Sent on"), + 'input_type' => 'date_tag', + 'input_name' => 'sent_on', + 'input_default_ge' => $filter->{'sent_on' . ':date::ge'}, + 'input_default_le' => $filter->{'sent_on' . ':date::le'}, + 'report_id' => 'sent_on', + 'active' => 1, + }, + attachment_names => { + 'position' => 7, + 'text' => t8("Attachments"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.attachments.name:substr::ilike', + 'input_default' => $filter->{attachments}->{'name:substr::ilike'}, + 'report_id' => 'attachment_names', + 'active' => 1, + }, + 'has_unprocessed_attachments' => { + 'position' => 8, + 'text' => t8("Has unprocessed attachments"), + 'input_type' => 'yes_no_tag', + 'input_name' => 'filter.has_unprocessed_attachments:eq_ignore_empty', + 'input_default' => $filter->{'has_unprocessed_attachments:eq_ignore_empty'}, + 'report_id' => 'has_unprocessed_attachments', + 'active' => 1, + }, + unprocessed_attachment_names => { + 'position' => 9, + 'text' => t8("Unprocessed Attachments"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.unprocessed_attachment_names:substr::ilike', + 'input_default' => $filter->{'unprocessed_attachment_names:substr::ilike'}, + 'report_id' => 'unprocessed_attachment_names', + 'active' => 1, + }, + status => { + 'position' => 10, + 'text' => t8("Status"), + 'input_type' => 'select_tag', + 'input_values' => [ + [ "", "" ], + [ "send_failed", t8("send failed") ], + [ "sent", t8("sent") ], + [ "imported", t8("imported") ] + ], + 'input_name' => 'filter.status', + 'input_default' => $filter->{'status'}, + 'report_id' => 'status', + 'active' => 1, + }, + extended_status => { + 'position' => 11, + 'text' => t8("Extended status"), + 'input_type' => 'input_tag', + 'input_name' => 'filter.extended_status:substr::ilike', + 'input_default' => $filter->{'extended_status:substr::ilike'}, + 'report_id' => 'extended_status', + 'active' => 1, + }, + record_type => { + 'position' => 12, + 'text' => t8("Record Type"), + 'input_type' => 'select_tag', + 'input_values' => [ + map {[ + $_->{record_type} => $_->{text}, + ]} + grep {!$_->{is_template}} + {}, + {text=> t8("Catch-all"), record_type => 'catch_all'}, + @{$params{record_types_with_info}} + ], + 'input_name' => 'filter.record_type:eq_ignore_empty', + 'input_default' => $filter->{'record_type:eq_ignore_empty'}, + 'report_id' => 'record_type', + 'active' => 1, + }, + 'linked' => { + 'position' => 13, + 'text' => t8("Linked"), + 'input_type' => 'yes_no_tag', + 'input_name' => 'filter.linked_to:eq_ignore_empty', + 'input_default' => $filter->{'linked_to:eq_ignore_empty'}, + 'report_id' => 'linked_to', + 'active' => 1, + }, + 'obsolete' => { + 'position' => 14, + 'text' => t8("Obsolete"), + 'input_type' => 'yes_no_tag', + 'input_name' => 'filter.obsolete:eq_ignore_empty', + 'input_default' => $filter->{'obsolete:eq_ignore_empty'}, + 'report_id' => 'obsolete', + 'active' => 1, + }, + ); # }}} + return \%default_filter_elements; +} + +sub filter { + my $filter = shift @_; + die "filter has to be a hash ref" if ref $filter ne 'HASH'; + my %params = validate_with( + params => \@_, + spec => { + record_types_with_info => { + type => ARRAYREF + }, + }, + allow_extra => 1, + ); + + my $filter_elements = get_default_filter_elements($filter, + record_types_with_info => delete $params{record_types_with_info}, + ); + + return SL::Presenter::Filter::create_filter($filter_elements, %params); +} + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::Presenter::Filter::EmailJournal - Presenter module for a generic filter on +EmailJournal. + +=head1 SYNOPSIS + + # in EmailJournal Controller + my $filter_html = SL::Presenter::Filter::EmailJournal::filter( + $::form->{filter}, + active_in_report => $::form->{active_in_report} + record_types_with_info => \@record_types_with_info, + ); + + +=head1 FUNCTIONS + +=over 4 + +=item C + +Returns a rendered version (actually an instance of +L) of a filter form for email journal. + +C<$filter> should be the C value of the last C<$::form>. This is used to +get the previous values of the input fields. + +C<%params> can include: + +=over 2 + + += item * record_types_with_info + +Is used to set the drop down for record type. + +=back + +Other C<%params> fields get forwarded to +C. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Tamino Steinert Etamino.steinert@tamino.stE + +=cut diff --git a/locale/de/all b/locale/de/all index b80beafdde..085cd92015 100755 --- a/locale/de/all +++ b/locale/de/all @@ -4378,7 +4378,6 @@ $ ./scripts/installation_check.pl', 'There are mulitple vendors selected' => 'Es sind mehrere Lieferanten ausgewählt', 'There are no documents in the WebDAV directory at the moment.' => 'Es befinden sich im WebDAV-Verzeichnis momentan keine Dokumente.', 'There are no entries in the background job history.' => 'Es gibt keine Einträge im Hintergrund-Job-Verlauf.', - 'There are no entries that match the filter.' => 'Es gibt keine Einträge, auf die der Filter zutrifft.', 'There are no items in stock.' => 'Dieser Artikel ist nicht eingelagert.', 'There are no items on your TODO list at the moment.' => 'Ihre Aufgabenliste enthält momentan keine Einträge.', 'There are no items selected' => 'Es wurden keine Positionen ausgewählt', @@ -4669,6 +4668,7 @@ $ ./scripts/installation_check.pl', 'Unlink bank transactions' => 'Bankverbuchung(en) rückgängig machen', 'Unlock System' => 'System entsperren', 'Unprocessed' => 'Nicht verarbeitet', + 'Unprocessed Attachments' => 'Nicht verarbeitete Anhänge', 'Unsuccessfully executed:\n' => 'Erfolglos ausgeführt:', 'Unsupported image type (supported types: #1)' => 'Nicht unterstützter Bildtyp (unterstützte Typen: #1)', 'Until' => 'Bis', diff --git a/locale/en/all b/locale/en/all index c25b890bb5..902700fdd5 100755 --- a/locale/en/all +++ b/locale/en/all @@ -4376,7 +4376,6 @@ $self->{texts} = { 'There are mulitple vendors selected' => '', 'There are no documents in the WebDAV directory at the moment.' => '', 'There are no entries in the background job history.' => '', - 'There are no entries that match the filter.' => '', 'There are no items in stock.' => '', 'There are no items on your TODO list at the moment.' => '', 'There are no items selected' => '', @@ -4667,6 +4666,7 @@ $self->{texts} = { 'Unlink bank transactions' => '', 'Unlock System' => '', 'Unprocessed' => '', + 'Unprocessed Attachments' => '', 'Unsuccessfully executed:\n' => '', 'Unsupported image type (supported types: #1)' => '', 'Until' => '', diff --git a/templates/design40_webpages/email_journal/_filter.html b/templates/design40_webpages/email_journal/_filter.html deleted file mode 100644 index 22ec3094bc..0000000000 --- a/templates/design40_webpages/email_journal/_filter.html +++ /dev/null @@ -1,93 +0,0 @@ -[% USE L %] -[% USE LxERP %] -[% USE HTML %] - -[% BLOCK filter_toggle_panel %] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[% LxERP.t8("From") %][% L.input_tag("filter.from:substr::ilike", filter.from_substr__ilike, size = 20) %] -
[% LxERP.t8("Recipients") %][% L.input_tag("filter.recipients:substr::ilike", filter.recipients_substr__ilike, size = 20) %] -
[% LxERP.t8("Sent on") %][% L.date_tag("filter.sent_on:date::ge", filter.sent_on_date__ge) %] [% LxERP.t8("To Date") %] [% L.date_tag("filter.sent_on:date::le", filter.sent_on_date__le) %] -
[% LxERP.t8("Status") %][% L.select_tag("filter.status:eq_ignore_empty", [ - [ "", "" ], - [ "send_failed", LxERP.t8("send failed") ], - [ "sent", LxERP.t8("sent") ], - [ "imported", LxERP.t8("imported") ] - ], default=filter.status_eq_ignore_empty) %] -
[% LxERP.t8("Record Type") %] - [% - SET record_type_options = []; - record_type_options.push({text=LxERP.t8("Catch-all"), record_type='catch_all'}); - FOREACH record_info = RECORD_TYPES_WITH_INFO; - IF (!record_info.is_template); - record_type_options.push(record_info); - END; - END; - %] - [% L.select_tag("filter.record_type:eq_ignore_empty", - record_type_options, - title_key = 'text', value_key = 'record_type', - with_empty=1, default=filter.record_type_eq_ignore_empty) %] -
[% LxERP.t8("Obsolete") %][% L.yes_no_tag("filter.obsolete:eq_ignore_empty", - filter.obsolete, with_empty=1, - default=filter.obsolete_eq_ignore_empty - ) %] -
[% LxERP.t8("Linked") %][% L.yes_no_tag("filter.linked_to:eq_ignore_empty", - filter.linked_to, with_empty=1, - default=filter.linked_to_eq_ignore_empty - ) %] -
[% LxERP.t8("Has unprocessed attachments") %][% L.yes_no_tag("filter.has_unprocessed_attachments:eq_ignore_empty", - filter.has_unprocessed_attachments, with_empty=1, - default=filter.has_unprocessed_attachments_eq_ignore_empty - ) %] -
- [% L.hidden_tag("sort_by", FORM.sort_by) %] - [% L.hidden_tag("sort_dir", FORM.sort_dir) %] - [% L.hidden_tag("page", FORM.page) %] -
[% L.button_tag('$("#filter_form").resetForm()', LxERP.t8('Reset')) %]
-[% END %] - -
-
- -[% INCLUDE common/toggle_panel.html %] - - - -
- - - diff --git a/templates/design40_webpages/email_journal/_report_bottom.html b/templates/design40_webpages/email_journal/_report_bottom.html new file mode 100644 index 0000000000..da08e0758e --- /dev/null +++ b/templates/design40_webpages/email_journal/_report_bottom.html @@ -0,0 +1,2 @@ +[% USE L %] +[%- L.paginate_controls(models=models) %] diff --git a/templates/design40_webpages/email_journal/_report_top.html b/templates/design40_webpages/email_journal/_report_top.html new file mode 100644 index 0000000000..e22c7c372c --- /dev/null +++ b/templates/design40_webpages/email_journal/_report_top.html @@ -0,0 +1,10 @@ +[% USE L %] + +[% BLOCK filter_toggle_panel %] +[%- FILTER_HTML %] +[% END %] + +
+ [% SET display_status = 'open' %] + [% INCLUDE 'common/toggle_panel.html' %] +
diff --git a/templates/design40_webpages/email_journal/list.html b/templates/design40_webpages/email_journal/list.html deleted file mode 100644 index dff920fd9a..0000000000 --- a/templates/design40_webpages/email_journal/list.html +++ /dev/null @@ -1,69 +0,0 @@ -[% USE HTML %] -[% USE L %] -[% USE P %] -[% USE LxERP %] - -

[% FORM.title %]

- -[% INCLUDE 'common/flash.html' %] -[% PROCESS 'email_journal/_filter.html' filter=SELF.models.filtered.laundered %] - -[% IF !ENTRIES.size %] -

[% LxERP.t8('There are no entries that match the filter.') %]

- -[% ELSE %] - - - - - [% IF SELF.can_view_all %] - - [% END %] - - - - - - - - - - - - - - - [% FOREACH entry = ENTRIES %] - - [% IF SELF.can_view_all %] - - [% END %] - [% action_show_link = SELF.url_for( - action => 'show', id => entry.id, - back_to => SELF.models.get_callback(), - ) %] - - - - - - - - - - - - - [% END %] - -
[% L.sortable_table_header("sender") %][% L.sortable_table_header("from") %][% L.sortable_table_header("recipients") %][% L.sortable_table_header("subject") %][% L.sortable_table_header("sent_on") %][% L.sortable_table_header("attachments") %][% L.sortable_table_header("status") %][% L.sortable_table_header("extended_status") %][% L.sortable_table_header("record_type") %][% L.sortable_table_header("obsolete") %][% L.sortable_table_header("linked_to") %][% L.sortable_table_header("has_unprocessed_attachments") %]
[% IF entry.sender %] [% HTML.escape(entry.sender.name) %] [% ELSE %] [% LxERP.t8("kivitendo") %] [% END %] [% HTML.escape(entry.from) %] [% HTML.escape(entry.recipients) %] [% HTML.escape(entry.subject) %] [% HTML.escape(entry.sent_on.to_lxoffice('precision' => 'second')) %] - [% FOREACH attachment = entry.attachments %] - [% HTML.escape(attachment.name) %] - [% END %] - [% P.email_journal.entry_status(entry) %] [% HTML.escape(entry.extended_status) %][% HTML.escape(RECORD_TYPES_TO_TEXT.${entry.record_type}) %][% HTML.escape(entry.obsolete_as_bool_yn) %] - [% P.record.simple_grouped_record_list(entry.linked_records) %] - [% HTML.escape(entry.has_unprocessed_attachments) %]
- -[% END %] - -[% L.paginate_controls %] diff --git a/templates/webpages/email_journal/_filter.html b/templates/webpages/email_journal/_filter.html deleted file mode 100644 index a5c9f14c34..0000000000 --- a/templates/webpages/email_journal/_filter.html +++ /dev/null @@ -1,87 +0,0 @@ -[%- USE L %][%- USE LxERP %][%- USE HTML %] -
-
- [% LxERP.t8('Show Filter') %] - [% IF SELF.filter_summary %]([% LxERP.t8("Current filter") %]: [% SELF.filter_summary %])[% END %] -
- - - -
diff --git a/templates/webpages/email_journal/_report_bottom.html b/templates/webpages/email_journal/_report_bottom.html new file mode 100644 index 0000000000..da08e0758e --- /dev/null +++ b/templates/webpages/email_journal/_report_bottom.html @@ -0,0 +1,2 @@ +[% USE L %] +[%- L.paginate_controls(models=models) %] diff --git a/templates/webpages/email_journal/_report_top.html b/templates/webpages/email_journal/_report_top.html new file mode 100644 index 0000000000..b347bfeffe --- /dev/null +++ b/templates/webpages/email_journal/_report_top.html @@ -0,0 +1,3 @@ +[% USE L %] + +[% FILTER_HTML %] diff --git a/templates/webpages/email_journal/list.html b/templates/webpages/email_journal/list.html deleted file mode 100644 index 5aa7a4bfde..0000000000 --- a/templates/webpages/email_journal/list.html +++ /dev/null @@ -1,74 +0,0 @@ -[% USE HTML %][% USE L %][% USE P %][% USE LxERP %] - -

[% FORM.title %]

- -[%- INCLUDE 'common/flash.html' %] - -[%- PROCESS 'email_journal/_filter.html' filter=SELF.models.filtered.laundered %] - -[% IF !ENTRIES.size %] -

- [%- LxERP.t8('There are no entries that match the filter.') %] -

- -[%- ELSE %] - - - - [% IF SELF.can_view_all %] - - [% END %] - - - - - - - - - - - - - - - - [%- FOREACH entry = ENTRIES %] - - [% IF SELF.can_view_all %] - - [% END %] - [% action_show_link = SELF.url_for( - action => 'show', id => entry.id, - back_to => SELF.models.get_callback(), - ) %] - - - - - - - - - - - - - [%- END %] - -
[% L.sortable_table_header("sender") %][% L.sortable_table_header("from") %][% L.sortable_table_header("recipients") %][% L.sortable_table_header("subject") %][% L.sortable_table_header("sent_on") %][% L.sortable_table_header("attachments") %][% L.sortable_table_header("status") %][% L.sortable_table_header("extended_status") %][% L.sortable_table_header("record_type") %][% L.sortable_table_header("obsolete") %][% L.sortable_table_header("linked_to") %][% L.sortable_table_header("has_unprocessed_attachments") %]
- [% IF entry.sender %] - [% HTML.escape(entry.sender.name) %] - [% ELSE %] - [% LxERP.t8("kivitendo") %] - [% END %] - [%- HTML.escape(entry.from) %][%- HTML.escape(entry.recipients) %][%- HTML.escape(entry.subject) %][%- HTML.escape(entry.sent_on.to_lxoffice('precision' => 'second')) %] - [% FOREACH attachment = entry.attachments %] - [% HTML.escape(attachment.name) %] - [% END %] - [% P.email_journal.entry_status(entry) %] [%- HTML.escape(entry.extended_status) %][% HTML.escape(RECORD_TYPES_TO_TEXT.${entry.record_type}) %][% HTML.escape(entry.obsolete_as_bool_yn) %] - [% P.record.simple_grouped_record_list(entry.linked_records) %] - [% HTML.escape(entry.has_unprocessed_attachments) %]
-[%- END %] - -[% L.paginate_controls %]