Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 8a92cb9b

Von Tamino Steinert vor etwa 1 Jahr hinzugefügt

  • ID 8a92cb9bcfc11748ead3eb7ede056b756eedca12
  • Vorgänger 50c0035f
  • Nachfolger 0ae2e90f

ImportRecordEmails: BJ zum importieren von Emails als Beleg-Grundlage

Verallgemeinerung und Ersetzung von BJ "ImportPurchaseInvoiceEmails"

Unterschiede anzeigen:

SL/BackgroundJob/ImportPurchaseInvoiceEmails.pm
1
package SL::BackgroundJob::ImportPurchaseInvoiceEmails;
2

  
3
use strict;
4
use warnings;
5

  
6
use parent qw(SL::BackgroundJob::Base);
7

  
8
use SL::IMAPClient;
9
use SL::DB::Manager::EmailImport;
10

  
11
sub sync_email_folder {
12
  my ($self) = @_;
13

  
14
  my $email_import = $self->{imap_client}->update_emails_from_folder(
15
    $self->{folder},
16
    {
17
      email_journal => {
18
        extended_status => 'purchase_invoice_import',
19
      },
20
    }
21
  );
22
  $self->{email_import} = $email_import;
23
  return unless $email_import;
24

  
25
  return "Created email import: " . $email_import->id;
26
}
27

  
28
sub delete_email_imports {
29
  my ($self) = @_;
30
  my $job_obj = $self->{job_obj};
31

  
32
  my $email_import_ids_to_delete =
33
    $job_obj->data_as_hash->{email_import_ids_to_delete} || [];
34

  
35
  my @deleted_email_imports_ids;
36
  foreach my $email_import_id (@$email_import_ids_to_delete) {
37
    my $email_import = SL::DB::Manager::EmailImport->find_by(id => $email_import_id);
38
    next unless $email_import;
39
    $email_import->delete(cascade => 1);
40
    push @deleted_email_imports_ids, $email_import_id;
41
  }
42
  return unless @deleted_email_imports_ids;
43

  
44
  return "Deleted email import(s): " . join(', ', @deleted_email_imports_ids);
45
}
46

  
47
sub clean_up_imported_emails {
48
  my ($self) = @_;
49

  
50
  $self->{imap_client}->clean_up_imported_emails_from_folder($self->{folder});
51

  
52
  return "Cleaned imported emails";
53
}
54

  
55
sub process_imported_purchase_invoice_emails {
56
  my ($self) = @_;
57
  return unless $self->{email_import};
58

  
59
  my $emails = $self->{email_import}->email_journals;
60

  
61
  foreach my $email (@$emails) {
62
    $email->process_attachments_as_purchase_invoices();
63
  }
64

  
65
  return "Processed imported emails";
66
}
67

  
68
sub run {
69
  my ($self, $job_obj) = @_;
70
  $self->{job_obj} = $job_obj;
71
  $self->{imap_client} = SL::IMAPClient->new(%{$::lx_office_conf{purchase_invoice_emails_imap}});
72
  $self->{folder} = $self->{job_obj}->data_as_hash->{folder};
73

  
74
  my @results;
75
  push @results, $self->delete_email_imports();
76
  push @results, $self->sync_email_folder();
77
  if ($self->{job_obj}->data_as_hash->{clean_up_imported_emails}) {
78
    push @results, $self->clean_up_imported_emails();
79
  }
80
  if ($self->{job_obj}->data_as_hash->{process_imported_purchase_invoice_emails}) {
81
    push @results, $self->process_imported_purchase_invoice_emails();
82
  }
83

  
84
  return join(". ", grep { $_ ne ''} @results);
85
}
86

  
87
1;
88

  
89
__END__
90

  
91
=encoding utf8
92

  
93
=head1 NAME
94

  
95
SL::BackgroundJob::ImportPurchaseInvoiceEmails - Background job for syncing
96
emails from a folder for purchase invoices .
97

  
98
=head1 SYNOPSIS
99

  
100
This background job is used to sync emails from a folder with purchase invoices.
101
It can be used to sync emails from a folder on a regular basis for multiple
102
folders . The folder to sync is specified in the data field 'folder' of the
103
background job, by default the folder 'base_folder' from
104
[purchase_invoice_emails_imap] in kivitendo.conf is used. Sub folders are
105
separated by a forward slash, e.g. 'INBOX/Archive'. Subfolders are not synced.
106
It can also remove emails from the folder which have been imported into kivitendo
107
by setting the data field 'clean_up_imported_emails' to a true value.
108

  
109
=head1 BUGS
110

  
111
Nothing here yet.
112

  
113
=head1 AUTHOR
114

  
115
Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
116

  
117
=cut
SL/BackgroundJob/ImportRecordEmails.pm
1
package SL::BackgroundJob::ImportRecordEmails;
2

  
3
use strict;
4
use warnings;
5

  
6
use parent qw(SL::BackgroundJob::Base);
7

  
8
use SL::IMAPClient;
9
use SL::DB::Manager::EmailImport;
10
use SL::Helper::EmailProcessing;
11
use SL::Presenter::Tag qw(link_tag);
12
use SL::Presenter::EscapedText qw(escape);
13

  
14
sub sync_record_email_folder {
15
  my ($self, $imap_client, $record_type, $folder) = @_;
16

  
17
  my $email_import = $imap_client->update_emails_from_folder(
18
    $folder,
19
    {
20
      email_journal => {
21
        # TODO: status => 'record_import',
22
        extended_status => 'record_import_' . "$record_type",
23
      },
24
    }
25
  );
26

  
27
  return $email_import;
28
}
29

  
30
sub delete_email_imports {
31
  my ($self) = @_;
32
  my $job_obj = $self->{job_obj};
33

  
34
  my $email_import_ids_to_delete =
35
    $job_obj->data_as_hash->{email_import_ids_to_delete} || [];
36

  
37
  my @deleted_email_imports_ids;
38
  foreach my $email_import_id (@$email_import_ids_to_delete) {
39
    my $email_import = SL::DB::Manager::EmailImport->find_by(id => $email_import_id);
40
    next unless $email_import;
41
    $email_import->delete(cascade => 1);
42
    push @deleted_email_imports_ids, $email_import_id;
43
  }
44
  return unless @deleted_email_imports_ids;
45

  
46
  return "Deleted email import(s): " . join(', ', @deleted_email_imports_ids) . ".\n";
47
}
48

  
49
sub run {
50
  my ($self, $job_obj) = @_;
51
  $self->{job_obj} = $job_obj;
52

  
53
  my $data = $job_obj->data_as_hash;
54

  
55
  my %configs = map { $_ => {
56
      %{$data->{records}->{$_}},
57
      config => $::lx_office_conf{"record_emails_imap/record/$_"}
58
        || $::lx_office_conf{record_emails_imap}
59
        || {},
60
    } } keys %{$data->{records}};
61

  
62
  my @results = ();
63
  push @results, $self->delete_email_imports();
64

  
65
  foreach my $import_key (keys %configs) {
66
    my @record_results = ();
67
    my $record_config = $configs{$import_key};
68
    my $imap_client = SL::IMAPClient->new(%{$record_config->{config}});
69
    my $record_folder = $record_config->{folder};
70

  
71
    my $email_import = $self->sync_record_email_folder(
72
      $imap_client, $import_key, $record_folder,
73
    );
74

  
75
    unless ($email_import) {
76
      push @results, "$import_key No emails to import";
77
      next;
78
    }
79
    push @record_results, "Created email import with id " . $email_import->id;
80

  
81
    if ($record_config->{process_imported_emails}) {
82
      my @function_names =
83
        ref $record_config->{process_imported_emails} eq 'ARRAY' ?
84
            @{$record_config->{process_imported_emails}}
85
          : ($record_config->{process_imported_emails});
86
      foreach my $email_journal (@{$email_import->email_journals}) {
87
        my $created_records = 0;
88
        foreach my $function_name (@function_names) {
89
          eval {
90
            my $processed = SL::Helper::EmailProcessing->process_attachments($function_name, $email_journal);
91
            $created_records += $processed;
92
            1;
93
          } or do {
94
            # TODO: link not shown as link
95
            my $email_journal_link = link_tag(
96
              $ENV{HTTP_ORIGIN} . $ENV{REQUEST_URI}
97
              . '?action=EmailJournal/show'
98
              . '&id=' . escape($email_journal->id)
99
              # text
100
              , $email_journal->id
101
            );
102
            push @record_results, "Error while processing email journal $email_journal_link attachments with $function_name: $@";
103
          };
104
        }
105
        if ($created_records) {
106
          $imap_client->set_flag_for_email(
107
            $email_journal, $record_config->{processed_imap_flag});
108
        } else {
109
          $imap_client->set_flag_for_email(
110
            $email_journal, $record_config->{not_processed_imap_flag});
111
        }
112

  
113
      }
114
      push @record_results, "Processed attachments with " . join(', ', @function_names) . ".";
115
    }
116

  
117
    push @results, join("\n- ", "$import_key :", @record_results);
118
  }
119

  
120
  return join("\n", grep { $_ ne ''} @results);
121
}
122

  
123
1;
124

  
125
__END__
126

  
127
=encoding utf8
128

  
129
=head1 NAME
130

  
131
SL::BackgroundJob::ImportPurchaseInvoiceEmails - Background job for syncing
132
emails from a folder for records.
133

  
134
=head1 SYNOPSIS
135

  
136
This background job syncs emails from a folder for records. The emails are
137
imported as email journals and can be processed with functions from
138
SL::Helper::EmailProcessing.
139

  
140
=head1 CONFIGURATION
141

  
142
In kivitendo.conf the settings for the IMAP server must be specified. The
143
default config is under [record_emails_imap]. The config for a specific record
144
type is under [record_emails_imap/record/<record_type>]. The config for a
145
specific record type overrides the default config.
146

  
147
In the data field 'records' of the background job, the record types to sync
148
emails for are specified. The key is the record type, the value is a hashref.
149
The hashref contains the following keys:
150

  
151
=over 4
152

  
153
=item folder
154

  
155
The folder to sync emails from. Sub folders are separated by a forward slash,
156
e.g. 'INBOX/Archive'. Subfolders are not synced.
157

  
158
=item process_imported_emails
159

  
160
The function name(s) to process the imported emails with. Multiple function
161
names can be specified as an arrayref. The function names are passed to
162
SL::Helper::EmailProcessing->process_attachments. The function names must be
163
implemented in SL::Helper::EmailProcessing.
164

  
165
=item processed_imap_flag
166

  
167
The IMAP flag to set for emails that were processed successfully.
168

  
169
=item not_processed_imap_flag
170

  
171
The IMAP flag to set for emails that were not processed successfully.
172

  
173
=back
174

  
175
=head1 METHODS
176

  
177

  
178

  
179
=head1 BUGS
180

  
181
Nothing here yet.
182

  
183
=head1 AUTHOR
184

  
185
Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
186

  
187
=cut
SL/DB/EmailJournalAttachment.pm
2 2

  
3 3
use strict;
4 4

  
5
use XML::LibXML;
6

  
7
use SL::ZUGFeRD;
8

  
9 5
use SL::DB::PurchaseInvoice;
10 6
use SL::DB::MetaSetup::EmailJournalAttachment;
11 7
use SL::DB::Manager::EmailJournalAttachment;
......
13 9

  
14 10
__PACKAGE__->meta->initialize;
15 11

  
16
sub create_ap_invoice {
17
  my ($self) = @_;
18

  
19
  my $content = $self->content; # scalar ref
20

  
21
  return unless $content =~ m/^%PDF/;
22

  
23
  my $zugferd_info = SL::ZUGFeRD->extract_from_pdf($content);
24
  return unless $zugferd_info->{result} == SL::ZUGFeRD::RES_OK();
25

  
26
  my $zugferd_xml = XML::LibXML->load_xml(string => $zugferd_info->{invoice_xml});
27

  
28
  return SL::DB::PurchaseInvoice->create_from_zugferd_xml($zugferd_xml)->save();
29
}
30

  
31 12
1;
SL/DB/PurchaseInvoice.pm
165 165

  
166 166
  my $ap_invoice = $class->new();
167 167

  
168
  $ap_invoice->import_zugferd_xml($zugferd_xml)->save();
168
  $ap_invoice->import_zugferd_xml($zugferd_xml);
169 169
}
170 170

  
171 171
sub create_ap_row {
SL/Helper/EmailProcessing.pm
1
package SL::Helper::EmailProcessing;
2

  
3
use strict;
4
use warnings;
5

  
6
use Carp;
7

  
8
use XML::LibXML;
9

  
10
use SL::ZUGFeRD;
11
use SL::Webdav;
12
use SL::File;
13

  
14
use SL::DB::PurchaseInvoice;
15

  
16
sub process_attachments {
17
  my ($self, $function_name, $email_journal, %params) = @_;
18

  
19
  unless ($self->can("process_attachments_$function_name")) {
20
    croak "Function not implemented for: $function_name";
21
  }
22
  $function_name = "process_attachments_$function_name";
23

  
24
  my $processed_count = 0;
25
  foreach my $attachment (@{$email_journal->attachments_sorted}) {
26
    my $processed = $self->$function_name($email_journal, $attachment, %params);
27
    $processed_count += $processed;
28
  }
29
  return $processed_count;
30
}
31

  
32
sub process_attachments_zugferd {
33
  my ($self, $email_journal, $attachment, %params) = @_;
34

  
35
  my $content = $attachment->content; # scalar ref
36

  
37
  return 0 unless $content =~ m/^%PDF/;
38

  
39
  my $zugferd_info = SL::ZUGFeRD->extract_from_pdf($content);
40
  return 0 unless $zugferd_info->{result} == SL::ZUGFeRD::RES_OK();
41

  
42
  my $zugferd_xml = XML::LibXML->load_xml(string => $zugferd_info->{invoice_xml});
43

  
44
  my $purchase_invoice = SL::DB::PurchaseInvoice->create_from_zugferd_xml($zugferd_xml)->save();
45

  
46
  $self->_add_attachment_to_record($email_journal, $attachment, $purchase_invoice);
47

  
48
  return 1;
49
}
50

  
51
sub _add_attachment_to_record {
52
  my ($self, $email_journal, $attachment, $record) = @_;
53

  
54
  # link to email journal
55
  $email_journal->link_to_record($record);
56

  
57
  # copy file to webdav folder
58
  if ($::instance_conf->get_webdav_documents) {
59
    my $record_type = $record->record_type;
60
    # TODO: file and webdav use different types for ap_transaction
61
    $record_type = 'accounts_payable' if $record_type eq 'ap_transaction';
62
    my $webdav = SL::Webdav->new(
63
      type     => $record_type,
64
      number   => $record->record_number,
65
    );
66
    my $webdav_file = SL::Webdav::File->new(
67
      webdav => $webdav,
68
      filename => $attachment->name,
69
    );
70
    eval {
71
      $webdav_file->store(data => \$attachment->content);
72
      1;
73
    } or do {
74
      die 'Storing the attachment file to the WebDAV folder failed: ' . $@;
75
    };
76
  }
77
  # copy file to doc storage
78
  if ($::instance_conf->get_doc_storage) {
79
    my $record_type = $record->record_type;
80
    # TODO: file and webdav use different types for ap_invoice
81
    $record_type = 'purchase_invoice' if $record_type eq 'ap_transaction';
82
    eval {
83
      SL::File->save(
84
        object_id     => $record->id,
85
        object_type   => $record_type,
86
        source        => 'uploaded',
87
        file_type     => 'document',
88
        file_name     => $attachment->name,
89
        file_contents => $attachment->content,
90
        mime_type     => $attachment->mime_type,
91
      );
92
      1;
93
    } or do {
94
      die 'Storing the ZUGFeRD file in the storage backend failed: ' . $@;
95
    };
96
  }
97

  
98
  my $new_ext_status = join(' ', $email_journal->extended_status,
99
    'created_record_' . $record->record_type);
100
  $email_journal->update_attributes(extended_status => $new_ext_status);
101

  
102
  # TODO: hardlink in db to email_journal
103
}
104

  
105
1;
config/kivitendo.conf.default
190 190
# If SSL is used, default port is 993
191 191
ssl = 1
192 192

  
193
# Import emails for purchase invoices
194
[purchase_invoice_emails_imap]
193
# Import emails for records with BackgroundJob ImportRecordEmails
194
# Config can specified per type with [record_emails_imap/record/<record_type>]
195
# More configuration is possible in the data field of the BJ, for more see
196
# SL::BackgroundJob::ImportRecordEmails
197
[record_emails_imap]
195 198
enabled = 0
196 199
hostname = localhost
197 200
username =
198 201
password =
199
# This folder can be managed with kivitendo through the background
200
# ImportPurchaseInvoiceEmails. Create no subfolder in the base folder by hand.
201
# Use / for subfolders.
202
# Use / for subfolders. Subfolders are not imported.
202 203
base_folder = INBOX
203 204
# Port only needs to be changed if it is not the default port.
204 205
# port = 993

Auch abrufbar als: Unified diff