Revision 1795164f
Von Tamino Steinert vor etwa 1 Jahr hinzugefügt
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
ImportRecordEmails: BJ zum importieren von Emails als Beleg-Grundlage