Revision 8a92cb9b
Von Tamino Steinert vor mehr als 1 Jahr hinzugefügt
| SL/BackgroundJob/ImportPurchaseInvoiceEmails.pm | ||
|---|---|---|
|
package SL::BackgroundJob::ImportPurchaseInvoiceEmails;
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
|
||
|
use parent qw(SL::BackgroundJob::Base);
|
||
|
|
||
|
use SL::IMAPClient;
|
||
|
use SL::DB::Manager::EmailImport;
|
||
|
|
||
|
sub sync_email_folder {
|
||
|
my ($self) = @_;
|
||
|
|
||
|
my $email_import = $self->{imap_client}->update_emails_from_folder(
|
||
|
$self->{folder},
|
||
|
{
|
||
|
email_journal => {
|
||
|
extended_status => 'purchase_invoice_import',
|
||
|
},
|
||
|
}
|
||
|
);
|
||
|
$self->{email_import} = $email_import;
|
||
|
return unless $email_import;
|
||
|
|
||
|
return "Created email import: " . $email_import->id;
|
||
|
}
|
||
|
|
||
|
sub delete_email_imports {
|
||
|
my ($self) = @_;
|
||
|
my $job_obj = $self->{job_obj};
|
||
|
|
||
|
my $email_import_ids_to_delete =
|
||
|
$job_obj->data_as_hash->{email_import_ids_to_delete} || [];
|
||
|
|
||
|
my @deleted_email_imports_ids;
|
||
|
foreach my $email_import_id (@$email_import_ids_to_delete) {
|
||
|
my $email_import = SL::DB::Manager::EmailImport->find_by(id => $email_import_id);
|
||
|
next unless $email_import;
|
||
|
$email_import->delete(cascade => 1);
|
||
|
push @deleted_email_imports_ids, $email_import_id;
|
||
|
}
|
||
|
return unless @deleted_email_imports_ids;
|
||
|
|
||
|
return "Deleted email import(s): " . join(', ', @deleted_email_imports_ids);
|
||
|
}
|
||
|
|
||
|
sub clean_up_imported_emails {
|
||
|
my ($self) = @_;
|
||
|
|
||
|
$self->{imap_client}->clean_up_imported_emails_from_folder($self->{folder});
|
||
|
|
||
|
return "Cleaned imported emails";
|
||
|
}
|
||
|
|
||
|
sub process_imported_purchase_invoice_emails {
|
||
|
my ($self) = @_;
|
||
|
return unless $self->{email_import};
|
||
|
|
||
|
my $emails = $self->{email_import}->email_journals;
|
||
|
|
||
|
foreach my $email (@$emails) {
|
||
|
$email->process_attachments_as_purchase_invoices();
|
||
|
}
|
||
|
|
||
|
return "Processed imported emails";
|
||
|
}
|
||
|
|
||
|
sub run {
|
||
|
my ($self, $job_obj) = @_;
|
||
|
$self->{job_obj} = $job_obj;
|
||
|
$self->{imap_client} = SL::IMAPClient->new(%{$::lx_office_conf{purchase_invoice_emails_imap}});
|
||
|
$self->{folder} = $self->{job_obj}->data_as_hash->{folder};
|
||
|
|
||
|
my @results;
|
||
|
push @results, $self->delete_email_imports();
|
||
|
push @results, $self->sync_email_folder();
|
||
|
if ($self->{job_obj}->data_as_hash->{clean_up_imported_emails}) {
|
||
|
push @results, $self->clean_up_imported_emails();
|
||
|
}
|
||
|
if ($self->{job_obj}->data_as_hash->{process_imported_purchase_invoice_emails}) {
|
||
|
push @results, $self->process_imported_purchase_invoice_emails();
|
||
|
}
|
||
|
|
||
|
return join(". ", grep { $_ ne ''} @results);
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
__END__
|
||
|
|
||
|
=encoding utf8
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
SL::BackgroundJob::ImportPurchaseInvoiceEmails - Background job for syncing
|
||
|
emails from a folder for purchase invoices .
|
||
|
|
||
|
=head1 SYNOPSIS
|
||
|
|
||
|
This background job is used to sync emails from a folder with purchase invoices.
|
||
|
It can be used to sync emails from a folder on a regular basis for multiple
|
||
|
folders . The folder to sync is specified in the data field 'folder' of the
|
||
|
background job, by default the folder 'base_folder' from
|
||
|
[purchase_invoice_emails_imap] in kivitendo.conf is used. Sub folders are
|
||
|
separated by a forward slash, e.g. 'INBOX/Archive'. Subfolders are not synced.
|
||
|
It can also remove emails from the folder which have been imported into kivitendo
|
||
|
by setting the data field 'clean_up_imported_emails' to a true value.
|
||
|
|
||
|
=head1 BUGS
|
||
|
|
||
|
Nothing here yet.
|
||
|
|
||
|
=head1 AUTHOR
|
||
|
|
||
|
Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
|
||
|
|
||
|
=cut
|
||
| SL/BackgroundJob/ImportRecordEmails.pm | ||
|---|---|---|
|
package SL::BackgroundJob::ImportRecordEmails;
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
|
||
|
use parent qw(SL::BackgroundJob::Base);
|
||
|
|
||
|
use SL::IMAPClient;
|
||
|
use SL::DB::Manager::EmailImport;
|
||
|
use SL::Helper::EmailProcessing;
|
||
|
use SL::Presenter::Tag qw(link_tag);
|
||
|
use SL::Presenter::EscapedText qw(escape);
|
||
|
|
||
|
sub sync_record_email_folder {
|
||
|
my ($self, $imap_client, $record_type, $folder) = @_;
|
||
|
|
||
|
my $email_import = $imap_client->update_emails_from_folder(
|
||
|
$folder,
|
||
|
{
|
||
|
email_journal => {
|
||
|
# TODO: status => 'record_import',
|
||
|
extended_status => 'record_import_' . "$record_type",
|
||
|
},
|
||
|
}
|
||
|
);
|
||
|
|
||
|
return $email_import;
|
||
|
}
|
||
|
|
||
|
sub delete_email_imports {
|
||
|
my ($self) = @_;
|
||
|
my $job_obj = $self->{job_obj};
|
||
|
|
||
|
my $email_import_ids_to_delete =
|
||
|
$job_obj->data_as_hash->{email_import_ids_to_delete} || [];
|
||
|
|
||
|
my @deleted_email_imports_ids;
|
||
|
foreach my $email_import_id (@$email_import_ids_to_delete) {
|
||
|
my $email_import = SL::DB::Manager::EmailImport->find_by(id => $email_import_id);
|
||
|
next unless $email_import;
|
||
|
$email_import->delete(cascade => 1);
|
||
|
push @deleted_email_imports_ids, $email_import_id;
|
||
|
}
|
||
|
return unless @deleted_email_imports_ids;
|
||
|
|
||
|
return "Deleted email import(s): " . join(', ', @deleted_email_imports_ids) . ".\n";
|
||
|
}
|
||
|
|
||
|
sub run {
|
||
|
my ($self, $job_obj) = @_;
|
||
|
$self->{job_obj} = $job_obj;
|
||
|
|
||
|
my $data = $job_obj->data_as_hash;
|
||
|
|
||
|
my %configs = map { $_ => {
|
||
|
%{$data->{records}->{$_}},
|
||
|
config => $::lx_office_conf{"record_emails_imap/record/$_"}
|
||
|
|| $::lx_office_conf{record_emails_imap}
|
||
|
|| {},
|
||
|
} } keys %{$data->{records}};
|
||
|
|
||
|
my @results = ();
|
||
|
push @results, $self->delete_email_imports();
|
||
|
|
||
|
foreach my $import_key (keys %configs) {
|
||
|
my @record_results = ();
|
||
|
my $record_config = $configs{$import_key};
|
||
|
my $imap_client = SL::IMAPClient->new(%{$record_config->{config}});
|
||
|
my $record_folder = $record_config->{folder};
|
||
|
|
||
|
my $email_import = $self->sync_record_email_folder(
|
||
|
$imap_client, $import_key, $record_folder,
|
||
|
);
|
||
|
|
||
|
unless ($email_import) {
|
||
|
push @results, "$import_key No emails to import";
|
||
|
next;
|
||
|
}
|
||
|
push @record_results, "Created email import with id " . $email_import->id;
|
||
|
|
||
|
if ($record_config->{process_imported_emails}) {
|
||
|
my @function_names =
|
||
|
ref $record_config->{process_imported_emails} eq 'ARRAY' ?
|
||
|
@{$record_config->{process_imported_emails}}
|
||
|
: ($record_config->{process_imported_emails});
|
||
|
foreach my $email_journal (@{$email_import->email_journals}) {
|
||
|
my $created_records = 0;
|
||
|
foreach my $function_name (@function_names) {
|
||
|
eval {
|
||
|
my $processed = SL::Helper::EmailProcessing->process_attachments($function_name, $email_journal);
|
||
|
$created_records += $processed;
|
||
|
1;
|
||
|
} or do {
|
||
|
# TODO: link not shown as link
|
||
|
my $email_journal_link = link_tag(
|
||
|
$ENV{HTTP_ORIGIN} . $ENV{REQUEST_URI}
|
||
|
. '?action=EmailJournal/show'
|
||
|
. '&id=' . escape($email_journal->id)
|
||
|
# text
|
||
|
, $email_journal->id
|
||
|
);
|
||
|
push @record_results, "Error while processing email journal $email_journal_link attachments with $function_name: $@";
|
||
|
};
|
||
|
}
|
||
|
if ($created_records) {
|
||
|
$imap_client->set_flag_for_email(
|
||
|
$email_journal, $record_config->{processed_imap_flag});
|
||
|
} else {
|
||
|
$imap_client->set_flag_for_email(
|
||
|
$email_journal, $record_config->{not_processed_imap_flag});
|
||
|
}
|
||
|
|
||
|
}
|
||
|
push @record_results, "Processed attachments with " . join(', ', @function_names) . ".";
|
||
|
}
|
||
|
|
||
|
push @results, join("\n- ", "$import_key :", @record_results);
|
||
|
}
|
||
|
|
||
|
return join("\n", grep { $_ ne ''} @results);
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
__END__
|
||
|
|
||
|
=encoding utf8
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
SL::BackgroundJob::ImportPurchaseInvoiceEmails - Background job for syncing
|
||
|
emails from a folder for records.
|
||
|
|
||
|
=head1 SYNOPSIS
|
||
|
|
||
|
This background job syncs emails from a folder for records. The emails are
|
||
|
imported as email journals and can be processed with functions from
|
||
|
SL::Helper::EmailProcessing.
|
||
|
|
||
|
=head1 CONFIGURATION
|
||
|
|
||
|
In kivitendo.conf the settings for the IMAP server must be specified. The
|
||
|
default config is under [record_emails_imap]. The config for a specific record
|
||
|
type is under [record_emails_imap/record/<record_type>]. The config for a
|
||
|
specific record type overrides the default config.
|
||
|
|
||
|
In the data field 'records' of the background job, the record types to sync
|
||
|
emails for are specified. The key is the record type, the value is a hashref.
|
||
|
The hashref contains the following keys:
|
||
|
|
||
|
=over 4
|
||
|
|
||
|
=item folder
|
||
|
|
||
|
The folder to sync emails from. Sub folders are separated by a forward slash,
|
||
|
e.g. 'INBOX/Archive'. Subfolders are not synced.
|
||
|
|
||
|
=item process_imported_emails
|
||
|
|
||
|
The function name(s) to process the imported emails with. Multiple function
|
||
|
names can be specified as an arrayref. The function names are passed to
|
||
|
SL::Helper::EmailProcessing->process_attachments. The function names must be
|
||
|
implemented in SL::Helper::EmailProcessing.
|
||
|
|
||
|
=item processed_imap_flag
|
||
|
|
||
|
The IMAP flag to set for emails that were processed successfully.
|
||
|
|
||
|
=item not_processed_imap_flag
|
||
|
|
||
|
The IMAP flag to set for emails that were not processed successfully.
|
||
|
|
||
|
=back
|
||
|
|
||
|
=head1 METHODS
|
||
|
|
||
|
|
||
|
|
||
|
=head1 BUGS
|
||
|
|
||
|
Nothing here yet.
|
||
|
|
||
|
=head1 AUTHOR
|
||
|
|
||
|
Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
|
||
|
|
||
|
=cut
|
||
| SL/DB/EmailJournalAttachment.pm | ||
|---|---|---|
|
|
||
|
use strict;
|
||
|
|
||
|
use XML::LibXML;
|
||
|
|
||
|
use SL::ZUGFeRD;
|
||
|
|
||
|
use SL::DB::PurchaseInvoice;
|
||
|
use SL::DB::MetaSetup::EmailJournalAttachment;
|
||
|
use SL::DB::Manager::EmailJournalAttachment;
|
||
| ... | ... | |
|
|
||
|
__PACKAGE__->meta->initialize;
|
||
|
|
||
|
sub create_ap_invoice {
|
||
|
my ($self) = @_;
|
||
|
|
||
|
my $content = $self->content; # scalar ref
|
||
|
|
||
|
return unless $content =~ m/^%PDF/;
|
||
|
|
||
|
my $zugferd_info = SL::ZUGFeRD->extract_from_pdf($content);
|
||
|
return unless $zugferd_info->{result} == SL::ZUGFeRD::RES_OK();
|
||
|
|
||
|
my $zugferd_xml = XML::LibXML->load_xml(string => $zugferd_info->{invoice_xml});
|
||
|
|
||
|
return SL::DB::PurchaseInvoice->create_from_zugferd_xml($zugferd_xml)->save();
|
||
|
}
|
||
|
|
||
|
1;
|
||
| SL/DB/PurchaseInvoice.pm | ||
|---|---|---|
|
|
||
|
my $ap_invoice = $class->new();
|
||
|
|
||
|
$ap_invoice->import_zugferd_xml($zugferd_xml)->save();
|
||
|
$ap_invoice->import_zugferd_xml($zugferd_xml);
|
||
|
}
|
||
|
|
||
|
sub create_ap_row {
|
||
| SL/Helper/EmailProcessing.pm | ||
|---|---|---|
|
package SL::Helper::EmailProcessing;
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
|
||
|
use Carp;
|
||
|
|
||
|
use XML::LibXML;
|
||
|
|
||
|
use SL::ZUGFeRD;
|
||
|
use SL::Webdav;
|
||
|
use SL::File;
|
||
|
|
||
|
use SL::DB::PurchaseInvoice;
|
||
|
|
||
|
sub process_attachments {
|
||
|
my ($self, $function_name, $email_journal, %params) = @_;
|
||
|
|
||
|
unless ($self->can("process_attachments_$function_name")) {
|
||
|
croak "Function not implemented for: $function_name";
|
||
|
}
|
||
|
$function_name = "process_attachments_$function_name";
|
||
|
|
||
|
my $processed_count = 0;
|
||
|
foreach my $attachment (@{$email_journal->attachments_sorted}) {
|
||
|
my $processed = $self->$function_name($email_journal, $attachment, %params);
|
||
|
$processed_count += $processed;
|
||
|
}
|
||
|
return $processed_count;
|
||
|
}
|
||
|
|
||
|
sub process_attachments_zugferd {
|
||
|
my ($self, $email_journal, $attachment, %params) = @_;
|
||
|
|
||
|
my $content = $attachment->content; # scalar ref
|
||
|
|
||
|
return 0 unless $content =~ m/^%PDF/;
|
||
|
|
||
|
my $zugferd_info = SL::ZUGFeRD->extract_from_pdf($content);
|
||
|
return 0 unless $zugferd_info->{result} == SL::ZUGFeRD::RES_OK();
|
||
|
|
||
|
my $zugferd_xml = XML::LibXML->load_xml(string => $zugferd_info->{invoice_xml});
|
||
|
|
||
|
my $purchase_invoice = SL::DB::PurchaseInvoice->create_from_zugferd_xml($zugferd_xml)->save();
|
||
|
|
||
|
$self->_add_attachment_to_record($email_journal, $attachment, $purchase_invoice);
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
sub _add_attachment_to_record {
|
||
|
my ($self, $email_journal, $attachment, $record) = @_;
|
||
|
|
||
|
# link to email journal
|
||
|
$email_journal->link_to_record($record);
|
||
|
|
||
|
# copy file to webdav folder
|
||
|
if ($::instance_conf->get_webdav_documents) {
|
||
|
my $record_type = $record->record_type;
|
||
|
# TODO: file and webdav use different types for ap_transaction
|
||
|
$record_type = 'accounts_payable' if $record_type eq 'ap_transaction';
|
||
|
my $webdav = SL::Webdav->new(
|
||
|
type => $record_type,
|
||
|
number => $record->record_number,
|
||
|
);
|
||
|
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 attachment file to the WebDAV folder failed: ' . $@;
|
||
|
};
|
||
|
}
|
||
|
# copy file to doc storage
|
||
|
if ($::instance_conf->get_doc_storage) {
|
||
|
my $record_type = $record->record_type;
|
||
|
# TODO: file and webdav use different types for ap_invoice
|
||
|
$record_type = 'purchase_invoice' if $record_type eq 'ap_transaction';
|
||
|
eval {
|
||
|
SL::File->save(
|
||
|
object_id => $record->id,
|
||
|
object_type => $record_type,
|
||
|
source => 'uploaded',
|
||
|
file_type => 'document',
|
||
|
file_name => $attachment->name,
|
||
|
file_contents => $attachment->content,
|
||
|
mime_type => $attachment->mime_type,
|
||
|
);
|
||
|
1;
|
||
|
} or do {
|
||
|
die 'Storing the ZUGFeRD file in the storage backend failed: ' . $@;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
my $new_ext_status = join(' ', $email_journal->extended_status,
|
||
|
'created_record_' . $record->record_type);
|
||
|
$email_journal->update_attributes(extended_status => $new_ext_status);
|
||
|
|
||
|
# TODO: hardlink in db to email_journal
|
||
|
}
|
||
|
|
||
|
1;
|
||
| config/kivitendo.conf.default | ||
|---|---|---|
|
# If SSL is used, default port is 993
|
||
|
ssl = 1
|
||
|
|
||
|
# Import emails for purchase invoices
|
||
|
[purchase_invoice_emails_imap]
|
||
|
# Import emails for records with BackgroundJob ImportRecordEmails
|
||
|
# Config can specified per type with [record_emails_imap/record/<record_type>]
|
||
|
# More configuration is possible in the data field of the BJ, for more see
|
||
|
# SL::BackgroundJob::ImportRecordEmails
|
||
|
[record_emails_imap]
|
||
|
enabled = 0
|
||
|
hostname = localhost
|
||
|
username =
|
||
|
password =
|
||
|
# This folder can be managed with kivitendo through the background
|
||
|
# ImportPurchaseInvoiceEmails. Create no subfolder in the base folder by hand.
|
||
|
# Use / for subfolders.
|
||
|
# Use / for subfolders. Subfolders are not imported.
|
||
|
base_folder = INBOX
|
||
|
# Port only needs to be changed if it is not the default port.
|
||
|
# port = 993
|
||
Auch abrufbar als: Unified diff
ImportRecordEmails: BJ zum importieren von Emails als Beleg-Grundlage