--- /dev/null
+package SL::Controller::EmailJournal;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Controller::Helper::GetModels;
+use SL::DB::Employee;
+use SL::DB::EmailJournal;
+use SL::DB::EmailJournalAttachment;
+use SL::Helper::Flash;
+use SL::Locale::String;
+use SL::System::TaskServer;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(entry) ],
+ 'scalar --get_set_init' => [ qw(models can_view_all filter_summary) ],
+);
+
+__PACKAGE__->run_before('add_stylesheet');
+
+#
+# actions
+#
+
+sub action_list {
+ my ($self) = @_;
+
+ $self->render('email_journal/list',
+ title => $::locale->text('Email journal'),
+ ENTRIES => $self->models->get,
+ MODELS => $self->models);
+}
+
+sub action_show {
+ my ($self) = @_;
+
+ my $back_to = $::form->{back_to} || $self->url_for(action => 'list');
+
+ $self->entry(SL::DB::EmailJournal->new(id => $::form->{id})->load);
+
+ if (!$self->can_view_all && ($self->entry->sender_id != SL::DB::Manager::Emplyee->current->id)) {
+ $::form->error(t8('You do not have permission to access this entry.'));
+ }
+
+ $self->render('email_journal/show',
+ title => $::locale->text('View sent email'),
+ back_to => $back_to);
+}
+
+sub action_download_attachment {
+ my ($self) = @_;
+
+ my $attachment = SL::DB::EmailJournalAttachment->new(id => $::form->{id})->load;
+
+ if (!$self->can_view_all && ($attachment->email_journal->sender_id != SL::DB::Manager::Emplyee->current->id)) {
+ $::form->error(t8('You do not have permission to access this entry.'));
+ }
+
+ $self->send_file(\$attachment->content, name => $attachment->name, type => $attachment->mime_type);
+}
+
+#
+# filters
+#
+
+sub add_stylesheet {
+ $::request->{layout}->use_stylesheet('email_journal.css');
+}
+
+#
+# helpers
+#
+
+sub init_can_view_all { $::auth->assert('admin', 1) }
+
+sub init_models {
+ my ($self) = @_;
+
+ my @where;
+ push @where, (sender_id => SL::DB::Manager::Employee->current->id) if !$self->can_view_all;
+
+ SL::Controller::Helper::GetModels->new(
+ controller => $self,
+ query => \@where,
+ with_objects => [ 'sender' ],
+ sorted => {
+ sender => t8('Sender'),
+ from => t8('From'),
+ recipients => t8('Recipients'),
+ subject => t8('Subject'),
+ sent_on => t8('Sent on'),
+ status => t8('Status'),
+ extended_status => t8('Extended status'),
+ },
+ );
+}
+
+sub init_filter_summary {
+ my ($self) = @_;
+
+ my $filter = $::form->{filter} || {};
+ my @filters = (
+ [ "from:substr::ilike", $::locale->text('From') ],
+ [ "recipients:substr::ilike", $::locale->text('Recipients') ],
+ [ "sent_on:date::ge", $::locale->text('Sent on') . " " . $::locale->text('From Date') ],
+ [ "sent_on:date::le", $::locale->text('Sent on') . " " . $::locale->text('To Date') ],
+ );
+
+ my @filter_strings = grep { $_ }
+ map { $filter->{ $_->[0] } ? $_->[1] . ' ' . $filter->{ $_->[0] } : undef }
+ @filters;
+
+ my %status = (
+ failed => $::locale->text('failed'),
+ ok => $::locale->text('succeeded'),
+ );
+ push @filter_strings, $status{ $filter->{'status:eq_ignore_empty'} } if $filter->{'status:eq_ignore_empty'};
+
+ return join ', ', @filter_strings;
+}
+
+1;
use SL::DB::MetaSetup::EmailJournal;
use SL::DB::Manager::EmailJournal;
+use SL::DB::Helper::AttrSorted;
__PACKAGE__->meta->add_relationship(
attachments => {
__PACKAGE__->meta->initialize;
+__PACKAGE__->attr_sorted('attachments');
+
1;
use parent qw(SL::DB::Helper::Manager);
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
sub object_class { 'SL::DB::EmailJournal' }
__PACKAGE__->make_manager_methods;
+sub _sort_spec {
+ return (
+ default => [ 'sent_on', 1 ],
+ columns => {
+ SIMPLE => 'ALL',
+ sender => 'sender.name',
+ },
+ );
+}
+
1;
--- /dev/null
+/* E-Mail-Journal */
+.email_journal_details tbody pre {
+ margin: 0px;
+}
+
+.email_journal_details tbody th {
+ text-align: right;
+ vertical-align: top;
+}
+
+.email_journal_details tbody td {
+ vertical-align: top;
+}
'Attach PDF:' => 'PDF anhängen',
'Attachment' => 'als Anhang',
'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.',
'Audit Control' => 'Bücherkontrolle',
'Aug' => 'Aug',
'Editable' => 'Bearbeitbar',
'Either there are no open invoices, or you have already initiated bank transfers with the open amounts for those that are still open.' => 'Entweder gibt es keine offenen Rechnungen, oder es wurden bereits Überweisungen über die offenen Beträge aller offenen Rechnungen erstellt.',
'Element disabled' => 'Element deaktiviert',
+ 'Email journal' => 'E-Mail-Journal',
'Employee' => 'Bearbeiter',
'Employee #1 saved!' => 'Benutzer #1 gespeichert!',
'Employee (database ID)' => 'Bearbeiter (Datenbank-ID)',
'Export date to' => 'Exportdatum bis',
'Extend automatically by n months' => 'Automatische Verlängerung um x Monate',
'Extended' => 'Gesamt',
+ 'Extended status' => 'Erweiterter Status',
'Extension Of Time' => 'Dauerfristverlängerung',
'Factor' => 'Faktor',
'Factor missing!' => 'Der Faktor fehlt.',
'Hardcopy' => 'Seite drucken',
'Has item type' => 'Hat Regeltypen',
'Has serial number' => 'Hat eine Serienummer',
+ 'Headers' => 'Kopfzeilen',
'Heading' => 'Überschrift',
'Help Template Variables' => 'Hilfe zu Dokumenten-Variablen',
'Help on column names' => 'Hilfe zu Spaltennamen',
'Receipt, payment, reconciliation' => 'Zahlungseingang, Zahlungsausgang, Kontenabgleich',
'Receipts' => 'Zahlungseingänge',
'Receivables' => 'Forderungen',
+ 'Recipients' => 'EmpfängerInnen',
'Reconcile' => 'Abgleichen',
'Reconciliation' => 'Kontenabgleich',
'Reconciliation with bank' => 'Kontenabgleich mit Bank',
'Sellprice for price group \'#1\'' => 'Verkaufspreis für Preisgruppe \'#1\'',
'Sellprice significant places' => 'Verkaufspreis: Nachkommastellen',
'Semicolon' => 'Semikolon',
+ 'Send PDF to support contract\'s contact person' => 'PDFs an Ansprechpersonen der Wartungsverträge schicken',
+ 'Sender' => 'AbsenderIn',
+ 'Sent on' => 'Verschickt am',
'Sep' => 'Sep',
'Separator' => 'Trennzeichen',
'Separator chararacter' => 'Feldtrennzeichen',
'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerplätze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
'Single quotes' => 'Einfache Anführungszeichen',
'Single values in item mode, cumulated values in invoice mode' => 'Einzelwerte im Artikelmodus, kumulierte Werte im Rechnungsmodus',
+ 'Size' => 'Größe',
'Sketch' => 'Skizze',
'Skip' => 'Überspringen',
'Skip entry' => 'Eintrag überspringen',
'There are invalid transactions in your database.' => 'Sie haben ungültige Buchungen in Ihrer Datenbank.',
'There are invoices which could not be paid by bank transaction #1 (Account number: #2, bank code: #3)!' => 'Einige Rechnungen konnten nicht durch die Bankbewegung #1 (Kontonummer: #2, Bankleitzahl: #3) bezahlt werden!',
'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 several options you can handle this problem, please select one:' => 'Bitte wählen Sie eine der folgenden Optionen, um mit dem Problem umzugehen:',
'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
'View background job history' => 'Hintergrund-Job-Verlauf anzeigen',
'View background jobs' => 'Hintergrund-Jobs anzeigen',
+ 'View sent email' => 'Verschickte E-Mail anzeigen',
'View warehouse content' => 'Lagerbestand ansehen',
'View/edit all employees sales documents' => 'Bearbeiten/ansehen der Verkaufsdokumente aller Mitarbeiter',
'Von Konto: ' => 'von Konto: ',
'You cannot create an invoice for delivery orders from different vendors.' => 'Sie können keine Rechnung aus Lieferscheinen von verschiedenen Lieferanten erstellen.',
'You cannot modify individual assigments from additional articles to line items.' => 'Eine individuelle Zuordnung der zusätzlichen Artikel zu Positionen kann nicht vorgenommen werden.',
'You cannot paste function blocks or sub function blocks if there is no section.' => 'Sie können keine Funktionsblöcke oder Unterfunktionsblöcke einfügen, wenn es noch keinen Abschnitt gibt.',
+ 'You do not have permission to access this entry.' => 'Sie verfügen nicht über die Berechtigung, auf diesen Eintrag zuzugreifen.',
'You do not have the permissions to access this function.' => 'Sie verfügen nicht über die notwendigen Rechte, um auf diese Funktion zuzugreifen.',
'You have entered or selected the following shipping address for this customer:' => 'Sie haben die folgende Lieferadresse eingegeben oder ausgewählt:',
'You have never worked with currencies.' => 'Sie haben noch nie mit Währungen gearbeitet.',
module: fu.pl
params:
action: search
+- parent: productivity_reports
+ id: productivity_reports_email_journal
+ name: Email journal
+ order: 200
+ module: controller.pl
+ params:
+ action: EmailJournal/list
- id: system
name: System
icon: system
--- /dev/null
+[%- USE L %][%- USE LxERP %][%- USE HTML %]
+<form action="controller.pl" method="post">
+ <div class="filter_toggle">
+ <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Show Filter') %]</a>
+ [% IF SELF.filter_summary %]([% LxERP.t8("Current filter") %]: [% SELF.filter_summary %])[% END %]
+ </div>
+
+ <div class="filter_toggle" style="display:none">
+ <a href="#" onClick="javascript:$('.filter_toggle').toggle()">[% LxERP.t8('Hide Filter') %]</a>
+ <table id="filter_table">
+ <tr>
+ <th align="right">[% LxERP.t8("From") %]</th>
+ <td>[% L.input_tag("filter.from:substr::ilike", filter.from_substr__ilike, size = 20) %]</td>
+ </tr>
+ <tr>
+ <th align="right">[% LxERP.t8("Recipients") %]</th>
+ <td>[% L.input_tag("filter.recipients:substr::ilike", filter.recipients_substr__ilike, size = 20) %]</td>
+ </tr>
+ <tr>
+ <th align="right">[% LxERP.t8("Sent on") %]</th>
+ <td>
+ [% 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) %]
+ </td>
+ </tr>
+ <tr>
+ <th align="right">[% LxERP.t8("Status") %]</th>
+ <td>[% L.select_tag("filter.status:eq_ignore_empty", [ [ "", "" ], [ "failed", LxERP.t8("failed") ], [ "ok", LxERP.t8("succeeded") ] ], default=filter.status_eq_ignore_empty) %]</td>
+ </tr>
+ </table>
+
+ [% L.hidden_tag("action", "EmailJournal/dispatch") %]
+ [% L.hidden_tag("sort_by", FORM.sort_by) %]
+ [% L.hidden_tag("sort_dir", FORM.sort_dir) %]
+ [% L.hidden_tag("page", FORM.page) %]
+ [% L.submit_tag("action_list", LxERP.t8("Continue"))%]
+
+ <a href="#" onClick="javascript:$('#filter_table input,#filter_table select').val("");">[% LxERP.t8("Reset") %]</a>
+
+ </div>
+
+</form>
--- /dev/null
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+<h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+[%- PROCESS 'email_journal/_filter.html' filter=SELF.models.filtered.laundered %]
+
+[% IF !ENTRIES.size %]
+ <p>
+ [%- LxERP.t8('There are no entries that match the filter.') %]
+ </p>
+
+[%- ELSE %]
+ <table id="email_journal_list" width="100%">
+ <thead>
+ <tr class="listheading">
+ [% IF SELF.can_view_all %]
+ <th>[% L.sortable_table_header("sender") %]</th>
+ [% END %]
+ <th>[% L.sortable_table_header("from") %]</th>
+ <th>[% L.sortable_table_header("recipients") %]</th>
+ <th>[% L.sortable_table_header("subject") %]</th>
+ <th>[% L.sortable_table_header("sent_on") %]</th>
+ <th>[% L.sortable_table_header("status") %]</th>
+ <th>[% L.sortable_table_header("extended_status") %]</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ [%- FOREACH entry = ENTRIES %]
+ <tr class="listrow[% IF entry.status != 'ok' %]_error[% END %]" id="email_journal_id_[% entry.id %]">
+ [% IF SELF.can_view_all %]
+ <td>
+ [% IF entry.sender %]
+ [% HTML.escape(entry.sender.name) %]
+ [% ELSE %]
+ [% LxERP.t8("kivitendo") %]
+ [% END %]
+ </td>
+ [% END %]
+ <td>
+ <a href="[% SELF.url_for(action => 'show', id => entry.id, back_to => SELF.get_callback) %]">
+ [%- HTML.escape(entry.from) %]
+ </a>
+ </td>
+ <td>[%- HTML.escape(entry.recipients) %]</td>
+ <td>
+ <a href="[% SELF.url_for(action => 'show', id => entry.id, back_to => SELF.get_callback) %]">
+ [%- HTML.escape(entry.subject) %]
+ </a>
+ </td>
+ <td>[%- HTML.escape(entry.sent_on.to_lxoffice('precision' => 'second')) %]</td>
+ <td>
+ [%- IF entry.status == 'ok' %]
+ [%- LxERP.t8('succeeded') %]
+ [% ELSE %]
+ [%- LxERP.t8('failed') %]
+ [%- END %]
+ </td>
+ <td>[%- HTML.escape(entry.extended_status) %]</td>
+ </tr>
+ [%- END %]
+ </tbody>
+ </table>
+[%- END %]
+
+[% L.paginate_controls %]
--- /dev/null
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+ <h1>[% FORM.title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+ <table id="email_journal_details" class="email_journal_details">
+ <tbody>
+ <tr class="listrow">
+ <th>[%- LxERP.t8("From") %]</th>
+ <td>[%- HTML.escape(SELF.entry.from) %]</td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Recipients") %]</th>
+ <td>[%- HTML.escape(SELF.entry.recipients) %]</td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Subject") %]</th>
+ <td>[%- HTML.escape(SELF.entry.subject) %]</td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Sent on") %]</th>
+ <td>[%- HTML.escape(SELF.entry.sent_on.to_lxoffice("precision" => "second")) %]</td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Status") %]</th>
+ <td>
+ [%- IF SELF.entry.status == "ok" %]
+ [%- LxERP.t8("succeeded") %]
+ [%- ELSE %]
+ [%- LxERP.t8("failed") %]
+ [%- END %]
+ </td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Extended status") %]</th>
+ <td><pre>[%- HTML.escape(SELF.entry.extended_status) %]</pre></td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Headers") %]</th>
+ <td><pre>[% HTML.escape(SELF.entry.headers) %]</pre></td>
+ </tr>
+
+ <tr class="listrow">
+ <th>[%- LxERP.t8("Body") %]</th>
+ <td><pre>[% HTML.escape(SELF.entry.body) %]</pre></td>
+ </tr>
+ </table>
+
+ [% SET attachments = SELF.entry.attachments_sorted %]
+ [% IF attachments.size %]
+ <h2>[% LxERP.t8("Attachments") %]</h2>
+
+ <table id="email_journal_details" class="email_journal_details">
+ <thead>
+ <tr>
+ <th>[% LxERP.t8("Attachment name") %]</th>
+ <th>[% LxERP.t8("MIME type") %]</th>
+ <th>[% LxERP.t8("Size") %]</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ [% FOREACH attachment = attachments %]
+ <tr class="listrow">
+ <td>[% L.link(SELF.url_for(action="download_attachment", id=attachment.id), attachment.name) %]</td>
+ <td>[% HTML.escape(attachment.mime_type) %]</td>
+ <td>[% HTML.escape(LxERP.format_amount(attachment.content.length, 0)) %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ [% END %]
+
+ <p>
+ <a href="[% back_to %]">[%- LxERP.t8("Back") %]</a>
+ </p>