From 610459d748bf329d1c8bbe363596a5b9d0fa4051 Mon Sep 17 00:00:00 2001 From: Johannes Grassler Date: Wed, 28 Feb 2024 16:42:35 +0000 Subject: [PATCH 1/4] Kreditorenbuchung: Metadaten zu Rechnungsposten Dieser Commit fuegt UI-Elemente zum Eintragen von Metadaten (Artikelbeschreibung, Menge, Artikelnummer) bei der Kreditorenbuchung hinzu. Auf diesem Weg koennen die wichtigsten Eckdaten der einzelnen Posten auf der Rechnung direkt in der Kreditorenbuchung mit abgelegt werden. Bei existierenden Kreditorenbuchungen, zu denen ein ZUGFeRD- oder XRechnung-Beleg vorliegt aber noch keine Metadaten gespeichert sind, koennen diese Metadaten direkt aus der Rechnung uebernommen werden. Die Metadaten werden in einem InvoiceItem-Objekt gespeichert, das auch mit der zugrundeliegenden Detailtransaktion in acc_trans verknuepft wird um die Zuordnung beim DATEV-Export zu vereinfachen. (cherry picked from commit 5af38af6c74bc47e01e3dcf8fce3a3bcdce71034) --- SL/AP.pm | 250 ++++++++++++++++++++++++- SL/Controller/ZUGFeRD.pm | 3 + SL/DB/MetaSetup/InvoiceItem.pm | 10 + bin/mozilla/ap.pl | 76 +++++++- locale/de/all | 2 + sql/Pg-upgrade2/invoice_metadata.sql | 8 + templates/webpages/ap/form_header.html | 33 +++- 7 files changed, 360 insertions(+), 22 deletions(-) create mode 100644 sql/Pg-upgrade2/invoice_metadata.sql diff --git a/SL/AP.pm b/SL/AP.pm index 31185ae2ac..dd75bb354b 100644 --- a/SL/AP.pm +++ b/SL/AP.pm @@ -36,6 +36,7 @@ package AP; use SL::DATEV qw(:CONSTANTS); +use SL::Helper::Flash qw(flash flash_later); use SL::DBUtils; use SL::IO; use SL::MoreCommon; @@ -48,6 +49,8 @@ use SL::DB::EmailJournal; use SL::DB::ValidityToken; use SL::Util qw(trim); use SL::DB; +use SL::XMLInvoice; +use SL::Locale::String qw(t8); use Data::Dumper; use List::Util qw(sum0); use strict; @@ -63,6 +66,225 @@ sub post_transaction { return $rc; } +sub post_save_metadata { + my ($self, $myconfig, $form, $provided_dbh, %params) = @_; + $main::lxdebug->enter_sub(); + + my $rc = SL::DB->client->with_transaction(\&_post_save_metadata, $self, $myconfig, $form, $provided_dbh, %params); + + $::lxdebug->leave_sub; + return $rc; +} + +# Save all metadata items +sub _post_save_metadata { + $main::lxdebug->enter_sub(); + my ($self, $myconfig, $form, $provided_dbh, %params) = @_; + my ($query, @values, $invoices, @processed_invoice_ids, $i); + + my $dbh = $provided_dbh || SL::DB->client->dbh; + + $query = qq| + SELECT * from acc_trans + WHERE (trans_id = $form->{id}) and (chart_link not like 'AP_tax%') + ORDER BY acc_trans_id + LIMIT $form->{rowcount}; + |; + + my $sth = prepare_execute_query($form, $dbh, $query); + + if ( $sth->rows != $form->{rowcount} ) { + SL::Helper::Flash::flash(t8("Form row count ($form->{rowcount}) does not match query row count ($sth->rowcount), not saving metadata.", $i)); + } + + $i=1; + while ( my $ref = $sth->fetchrow_hashref("NAME_lc") ) { + my %params_row = %params; + $params_row{'acc_trans_id'} = $ref->{"acc_trans_id"}; + $params_row{'row'} = $i; + + my $rc = SL::DB->client->with_transaction(\&_save_metadata_item, $self, $myconfig, $form, $provided_dbh, %params_row); + push @processed_invoice_ids, $form->{"invoice_id_$i"}; + $i++; + } + + + # search for orphaned invoice items + unless ( scalar @processed_invoice_ids == 0 ) { + $query = sprintf 'SELECT id FROM invoice WHERE trans_id = ? AND NOT id IN (%s)', join ', ', ("?") x scalar @processed_invoice_ids; + @values = (conv_i($form->{id}), map { conv_i($_) } @processed_invoice_ids); + my @orphaned_ids = map { $_->{id} } selectall_hashref_query($form, $dbh, $query, @values); + if (scalar @orphaned_ids) { + # clean up invoice items + $query = sprintf 'DELETE FROM invoice WHERE id IN (%s)', join ', ', ("?") x scalar @orphaned_ids; + do_query($form, $dbh, $query, @orphaned_ids); + } + } + + $main::lxdebug->leave_sub(); +} + +# Save a single metadata item +sub _save_metadata_item { + $main::lxdebug->enter_sub(); + my ($self, $myconfig, $form, $provided_dbh, %params) = @_; + my $acc_trans_id = defined $params{'acc_trans_id'} ? $params{'acc_trans_id'} : undef; + my $i = $params{'row'}; + + my ($invoice, $query, $parts_id, $vendor_partno, @values); + + # Look the Invoice object up in case it exists already + my $dbh = $provided_dbh || SL::DB->client->dbh; + $invoice = SL::DB::Manager::InvoiceItem->get_first( + where => [ + trans_id => conv_i($form->{id}), + position => $i + ] + ); + $form->{"invoice_id_$i"} = ref $invoice eq 'SL::DB::InvoiceItem' ? $invoice->{id} : undef; + + if ( (!$form->{"invoice_id_$i"}) and (!$params{'acc_trans_id'}) ) { + SL::Helper::Flash::flash(t8("No existing invoice item found for row #1 and no acc_trans_id provided. Cannot create invoice item without acc_trans_id.", $i)); + } + + # create detail record in invoice table + if (!$form->{"invoice_id_$i"}) { + # there is no persistent id, therefore create one with all necessary constraints + my $q_invoice_id = qq|SELECT nextval('invoiceid')|; + my $h_invoice_id = prepare_query($form, $dbh, $q_invoice_id); + do_statement($form, $h_invoice_id, $q_invoice_id); + $form->{"invoice_id_$i"} = $h_invoice_id->fetchrow_array(); + my $q_create_invoice_id = qq|INSERT INTO invoice (id, trans_id, acc_trans_id, position) values (?, ?, ?, ?)|; + do_query($form, $dbh, $q_create_invoice_id, + conv_i($form->{"invoice_id_$i"}), + conv_i($form->{id}), $acc_trans_id, + conv_i($i)); + $h_invoice_id->finish(); + } + + # Try to look up internal article number if a vendor part number exists + if ( $form->{"vendor_partno_$i"} ) { + my $makemodel = SL::DB::Manager::MakeModel->get_first( + where => [ + make => conv_i($form->{vendor_id}), + model => $form->{"vendor_partno_$i"} + ] + ); + $parts_id = ref $makemodel eq 'SL::DB::MakeModel' ? $makemodel->{parts_id} : undef; + } + + $query = <{"description_$i"}, $form->{"quantity_$i"} * 1, + $form->{"vendor_partno_$i"}, + conv_i($form->{"project_id_$i"}), + conv_i($form->{"invoice_id_$i"})); + do_query($form, $dbh, $query, @values); + + $main::lxdebug->leave_sub(); + return unless $i; +} + +sub post_update_zugferd { + my ($self, $myconfig, $form, $provided_dbh, %params) = @_; + $main::lxdebug->enter_sub(); + + my $rc = SL::DB->client->with_transaction(\&_post_update_zugferd, $self, $myconfig, $form, $provided_dbh, %params); + + $::lxdebug->leave_sub; + return $rc; +} + +sub _post_update_zugferd { + $main::lxdebug->enter_sub(); + my ($self, $myconfig, $form, $provided_dbh, %params) = @_; + my $dbh = $provided_dbh || SL::DB->client->dbh; + my $data; + my %res; + my @items; + + foreach my $file ( SL::File->get_all(object_type => "purchase_invoice", object_id => $form->{"id"}) ) { + my $filename = $file->file_name; + + + if ( $filename =~ m/pdf$/i ) { + %res = %{SL::ZUGFeRD->extract_from_pdf($file->get_file)}; + } elsif ( $filename =~ m/xml$/i ){ + %res = %{SL::ZUGFeRD->extract_from_xml($file->get_content)}; + } + + + # first parseable XML bill wins if there are multiple attachments + last if (defined $res{'result'}) and ($res{'result'} == SL::ZUGFeRD::RES_OK()); + } + + # no useable XML payload attach to invoice + return if ( (!defined $res{'result'}) or ($res{'result'} != SL::ZUGFeRD::RES_OK()) ); + + $data = $res{'invoice_xml'}; + @items = @{$data->items}; + + # sanity check: make sure the number of rows lines up + return if ( (scalar @items) != ($form->{rowcount} - 1) ); + + for my $i (0 .. $form->{rowcount} - 2) { + my $j = $i+1; + $form->{"description_$j"} = $items[$i]{'description'}; + $form->{"quantity_$j"} = conv_i($items[$i]{'quantity'}); + $form->{"vendor_partno_$j"} = $items[$i]{'vendor_partno'}; + } + + $::lxdebug->leave_sub; +} + + +sub load_metadata { + $main::lxdebug->enter_sub(); + + my ($self, $myconfig, $form) = @_; + + # connect to database + my $dbh = SL::DB->client->dbh; + + # retrieve invoice + my $query = qq|SELECT position, parts_id, vendor_partno, description, qty + FROM invoice + WHERE trans_id = ? + ORDER BY position;|; + + my $sth = prepare_execute_query($form, $dbh, $query, conv_i($form->{"id"})); + + my $i=1; + while ( my $ref = $sth->fetchrow_hashref("NAME_lc") ) { + # pretty print internal part number + if ( $ref->{"parts_id"} ) { + my $part = SL::DB::Manager::Part->get_first( + where => [ id => $ref->{"parts_id"} ] + ); + if ( ref $part eq "SL::DB::Part" ) { + $form->{"partno_$i"} = $part->{partnumber}; + $form->{"parts_id_$i"} = $part->{id}; + } else { + $form->{"partno_$i"} = undef; + } + } + $form->{"description_$i"} = $ref->{description}; + $form->{"quantity_$i"} = $ref->{qty}; + $form->{"vendor_partno_$i"} = $ref->{vendor_partno}; + + $i++; + } + + $sth->finish(); + + $main::lxdebug->leave_sub(); +} + sub _post_transaction { my ($self, $myconfig, $form, $provided_dbh, %params) = @_; @@ -210,20 +432,28 @@ sub _post_transaction { # add individual transactions for my $i (1 .. $form->{rowcount}) { + my $position = $i; + if ($form->{"amount_$i"} != 0) { - my $project_id; + my ($project_id, $new_acc_trans, %params_row); + %params_row = %params; + + $params_row{'row'} = $i; + $project_id = conv_i($form->{"project_id_$i"}); # insert detail records in acc_trans - $query = - qq|INSERT INTO acc_trans | . - qq| (trans_id, chart_id, amount, transdate, project_id, taxkey, tax_id, chart_link)| . - qq|VALUES (?, ?, ?, ?, ?, ?, ?, (SELECT c.link FROM chart c WHERE c.id = ?))|; - @values = ($form->{id}, $form->{"AP_amount_chart_id_$i"}, - $form->{"amount_$i"}, conv_date($form->{transdate}), - $project_id, $form->{"taxkey_$i"}, conv_i($form->{"tax_id_$i"}), - $form->{"AP_amount_chart_id_$i"}); - do_query($form, $dbh, $query, @values); + $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $form->{id}, + chart_id => $form->{"AP_amount_chart_id_$i"}, + chart_link => $form->{"AP_amount_chart_id_$i"}, + amount => $form->{"amount_$i"}, + transdate => conv_date($form->{transdate}), + project_id => $project_id, + taxkey => $form->{"taxkey_$i"}, + tax_id => conv_i($form->{"tax_id_$i"}))->save; + $params_row{'acc_trans_id'} = $new_acc_trans->{'acc_trans_id'}; + + my $rc = SL::DB->client->with_transaction(\&_save_metadata_item, $self, $myconfig, $form, $provided_dbh, %params_row); if ($form->{"tax_$i"} != 0 && !$form->{"reverse_charge_$i"}) { # insert detail records in acc_trans diff --git a/SL/Controller/ZUGFeRD.pm b/SL/Controller/ZUGFeRD.pm index dff05e12de..88a5e8057f 100644 --- a/SL/Controller/ZUGFeRD.pm +++ b/SL/Controller/ZUGFeRD.pm @@ -276,6 +276,9 @@ sub build_ap_transaction_form_defaults { $item_form{"previous_AP_amount_chart_id_${row}"} = $default_ap_amount_chart->id; $item_form{"amount_${row}"} = $net_total; $item_form{"taxchart_${row}"} = $tax->id . '--' . $tax->rate; + $item_form{"description_${row}"} = $item{'description'}; + $item_form{"quantity_${row}"} = $item{'quantity'}; + $item_form{"vendor_partno_${row}"} = $item{'vendor_partno'}; } $item_form{rowcount} = $row; diff --git a/SL/DB/MetaSetup/InvoiceItem.pm b/SL/DB/MetaSetup/InvoiceItem.pm index 173392950d..af4ad9e4fd 100644 --- a/SL/DB/MetaSetup/InvoiceItem.pm +++ b/SL/DB/MetaSetup/InvoiceItem.pm @@ -9,6 +9,7 @@ use parent qw(SL::DB::Object); __PACKAGE__->meta->table('invoice'); __PACKAGE__->meta->columns( + acc_trans_id => { type => 'integer' }, active_discount_source => { type => 'text', default => '', not_null => 1 }, active_price_source => { type => 'text', default => '', not_null => 1 }, allocated => { type => 'float', precision => 4, scale => 4 }, @@ -46,13 +47,22 @@ __PACKAGE__->meta->columns( trans_id => { type => 'integer' }, transdate => { type => 'text' }, unit => { type => 'varchar', length => 20 }, + vendor_partno => { type => 'text' }, ); __PACKAGE__->meta->primary_key_columns([ 'id' ]); +__PACKAGE__->meta->unique_keys([ 'acc_trans_id' ]); + __PACKAGE__->meta->allow_inline_column_values(1); __PACKAGE__->meta->foreign_keys( + acc_transaction => { + class => 'SL::DB::AccTransaction', + key_columns => { acc_trans_id => 'acc_trans_id' }, + rel_type => 'one to one', + }, + expense_chart => { class => 'SL::DB::Chart', key_columns => { expense_chart_id => 'id' }, diff --git a/bin/mozilla/ap.pl b/bin/mozilla/ap.pl index 6e7331af17..9d32d4e2d2 100644 --- a/bin/mozilla/ap.pl +++ b/bin/mozilla/ap.pl @@ -335,6 +335,7 @@ sub edit { # evaluated. my $form = $main::form; + my %myconfig = %main::myconfig; $form->{title} = "Edit"; @@ -407,6 +408,7 @@ sub create_links { $form->{employee} = "$form->{employee}--$form->{employee_id}"; AP->setup_form($form); + AP->load_metadata(\%myconfig, $form); $main::lxdebug->leave_sub(); } @@ -531,6 +533,7 @@ sub form_header { # format amounts $form->{"amount_$i"} = $form->format_amount(\%myconfig, $form->{"amount_$i"}, 2); + $form->{"quantity_$i"} = $form->format_amount(\%myconfig, $form->{"quantity_$i"}, 2); $form->{"tax_$i"} = $form->format_amount(\%myconfig, $form->{"tax_$i"}, 2); my ($default_taxchart, $taxchart_to_use); @@ -723,6 +726,42 @@ sub show_draft { update(); } +sub save_metadata { + my %params = @_; + + $main::lxdebug->enter_sub(); + + my $form = $main::form; + my %myconfig = %main::myconfig; + + $main::auth->assert('ap_transactions'); + + AP->post_save_metadata(\%myconfig, \%$form); + AP->load_metadata(\%myconfig, $form); + + display_form(); + + $main::lxdebug->leave_sub(); +} + +sub update_zugferd { + my %params = @_; + + $main::lxdebug->enter_sub(); + + my $form = $main::form; + my %myconfig = %main::myconfig; + + $main::auth->assert('ap_transactions'); + + AP->post_update_zugferd(\%myconfig, $form); + + create_links(); + display_form(); + + $main::lxdebug->leave_sub(); +} + sub update { my %params = @_; @@ -806,6 +845,11 @@ sub update { $form->{oldinvtotal} = $form->{invtotal}; $form->{oldtotalpaid} = $totalpaid; + if ( $params{save_metadata} == 1 ) { + AP->post_save_metadata(\%myconfig, \%$form); + AP->load_metadata(\%myconfig, $form); + } + display_form(); $main::lxdebug->leave_sub(); @@ -1609,13 +1653,23 @@ sub setup_ap_display_form_action_bar { for my $bar ($::request->layout->get('actionbar')) { $bar->add( - action => [ - t8('Update'), - submit => [ '#form', { action => "update" } ], - id => 'update_button', - checks => [ 'kivi.validate_form' ], - accesskey => 'enter', - disabled => !$may_edit_create ? t8('You must not change this AP transaction.') : undef, + combobox => [ + action => [ + t8('Update'), + submit => [ '#form', { action => "update" } ], + id => 'update_button', + checks => [ 'kivi.validate_form' ], + accesskey => 'enter', + disabled => !$may_edit_create ? t8('You must not change this AP transaction.') : undef, + ], + action => [ t8('Get metadata from attached XRechnung/ZUGFeRD document'), + submit => [ '#form', { action => "update_zugferd" } ], + disabled => !$may_edit_create ? t8('You must not change this AP transaction.') + : ($::form->{id} && $change_never) ? t8('Changing invoices has been disabled in the configuration.') + : ($::form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.') + : !$::form->{id} ? t8('This invoice has not been posted yet.') + : undef, + ], ], combobox => [ @post_entries, @@ -1642,6 +1696,14 @@ sub setup_ap_display_form_action_bar { : undef, only_if => $::instance_conf->get_is_show_mark_as_paid, ], + action => [ t8('Save metadata'), + submit => [ '#form', { action => "save_metadata" } ], + disabled => !$may_edit_create ? t8('You must not change this AP transaction.') + : ($::form->{id} && $change_never) ? t8('Changing invoices has been disabled in the configuration.') + : ($::form->{id} && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.') + : !$::form->{id} ? t8('This invoice has not been posted yet.') + : undef, + ], ], # end of combobox "Post" combobox => [ diff --git a/locale/de/all b/locale/de/all index 9cbc162135..35a97ce67c 100644 --- a/locale/de/all +++ b/locale/de/all @@ -1877,6 +1877,7 @@ $ ./scripts/installation_check.pl', 'Generic Presenter Test' => 'Generisch-Presenter-Test', 'Generic email send address for this record type.' => 'Generischer E-Mail-Absenderadresse für diesen Belegtyp.', 'Germany' => 'Deutschland', + 'Get metadata from attached XRechnung/ZUGFeRD document' => 'Metadaten aus Dokumentenanhang (XRechnung/ZUGFeRD) übernehmen', 'Get one order' => 'Hole eine Bestellung', 'Get one order by shopordernumber' => 'Hole eine Bestellung über Shopbestellnummer', 'Get one shoporder' => 'Hole eine Bestellung', @@ -3464,6 +3465,7 @@ $ ./scripts/installation_check.pl', 'Save document in WebDAV repository' => 'Dokument in WebDAV-Ablage speichern', 'Save draft' => 'Entwurf speichern', 'Save invoices' => 'Rechnungen speichern', + 'Save metadata' => 'Metadaten speichern', 'Save profile' => 'Profil speichern', 'Save proposals' => 'Vorschläge speichern', 'Save settings as' => 'Einstellungen speichern unter', diff --git a/sql/Pg-upgrade2/invoice_metadata.sql b/sql/Pg-upgrade2/invoice_metadata.sql new file mode 100644 index 0000000000..6dd05e61ef --- /dev/null +++ b/sql/Pg-upgrade2/invoice_metadata.sql @@ -0,0 +1,8 @@ +-- @tag: invoice_metadat +-- @description: Add vendor_partno and a foreign key for referencing detail transactions to invoice + +ALTER TABLE invoice ADD COLUMN acc_trans_id integer; +ALTER TABLE invoice ADD COLUMN vendor_partno text; + +ALTER TABLE invoice ADD CONSTRAINT acc_trans_id_fkey FOREIGN KEY (acc_trans_id) REFERENCES public.acc_trans(acc_trans_id); +ALTER TABLE invoice ADD CONSTRAINT acc_trans_id_key UNIQUE (acc_trans_id); diff --git a/templates/webpages/ap/form_header.html b/templates/webpages/ap/form_header.html index ec1a17a191..9837d8d6f1 100644 --- a/templates/webpages/ap/form_header.html +++ b/templates/webpages/ap/form_header.html @@ -219,7 +219,10 @@

- + + + + @@ -227,13 +230,33 @@

[% FOREACH i IN [1..rowcount] %] - + + + + @@ -250,7 +273,7 @@

From 81b92422ceda061e559f752dd120d3ead0ac738a Mon Sep 17 00:00:00 2001 From: Johannes Grassler Date: Fri, 1 Mar 2024 22:09:24 +0000 Subject: [PATCH 2/4] DATEV-Export um Metadaten erweitert Dieser Commit gibt die Einzelposten-Metadaten aus der Kreditorenbuchung (Beschreibung und Menge) im DATEV-Export als zusaetzliche Beschreibungsfelder aus (Spalte 21-24). (cherry picked from commit dae4e037616e613147c0c6d2c343f8c72abd2553) --- SL/DATEV.pm | 18 +++++++++++++++--- SL/DATEV/CSV.pm | 29 ++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/SL/DATEV.pm b/SL/DATEV.pm index 86cb729bc6..032dcfe7b7 100644 --- a/SL/DATEV.pm +++ b/SL/DATEV.pm @@ -472,7 +472,8 @@ sub generate_datev_data { ar.department_id, ar.notes, project.projectnumber as projectnumber, project.description as projectdescription, - department.description as departmentdescription + department.description as departmentdescription, + iv.description as ivdescription, iv.qty as ivqty FROM acc_trans ac LEFT JOIN ar ON (ac.trans_id = ar.id) LEFT JOIN customer ct ON (ar.customer_id = ct.id) @@ -481,6 +482,7 @@ sub generate_datev_data { LEFT JOIN chart tc ON (t.chart_id = tc.id) LEFT JOIN department ON (department.id = ar.department_id) LEFT JOIN project ON (project.id = ar.globalproject_id) + LEFT JOIN invoice iv ON (iv.acc_trans_id = ac.acc_trans_id) WHERE (ar.id IS NOT NULL) AND $fromto $trans_id_filter @@ -501,7 +503,8 @@ sub generate_datev_data { ap.department_id, ap.notes, project.projectnumber as projectnumber, project.description as projectdescription, - department.description as departmentdescription + department.description as departmentdescription, + iv.description, iv.qty FROM acc_trans ac LEFT JOIN ap ON (ac.trans_id = ap.id) LEFT JOIN vendor ct ON (ap.vendor_id = ct.id) @@ -510,6 +513,7 @@ sub generate_datev_data { LEFT JOIN chart tc ON (t.chart_id = tc.id) LEFT JOIN department ON (department.id = ap.department_id) LEFT JOIN project ON (project.id = ap.globalproject_id) + LEFT JOIN invoice iv ON (iv.acc_trans_id = ac.acc_trans_id) WHERE (ap.id IS NOT NULL) AND $fromto $trans_id_filter @@ -530,13 +534,15 @@ sub generate_datev_data { gl.department_id, gl.notes, '' as projectnumber, '' as projectdescription, - department.description as departmentdescription + department.description as departmentdescription, + iv.description, iv.qty FROM acc_trans ac LEFT JOIN gl ON (ac.trans_id = gl.id) LEFT JOIN chart c ON (ac.chart_id = c.id) LEFT JOIN tax t ON (ac.tax_id = t.id) LEFT JOIN chart tc ON (t.chart_id = tc.id) LEFT JOIN department ON (department.id = gl.department_id) + LEFT JOIN invoice iv ON (iv.acc_trans_id = ac.acc_trans_id) WHERE (gl.id IS NOT NULL) AND $fromto $trans_id_filter @@ -846,6 +852,12 @@ sub generate_datev_lines { } else { $soll = $i; } + if ($transaction->[$i]->{'ivdescription'}) { + $datev_data{description} = $transaction->[$i]->{'ivdescription'}; + } + if ($transaction->[$i]->{'ivqty'}) { + $datev_data{quantity} = $transaction->[$i]->{'ivqty'}; + } } if ($trans_lines >= 2) { diff --git a/SL/DATEV/CSV.pm b/SL/DATEV/CSV.pm index 79e6b4d2e0..641054e71b 100644 --- a/SL/DATEV/CSV.pm +++ b/SL/DATEV/CSV.pm @@ -160,17 +160,36 @@ my @kivitendo_to_datev = ( # "8DB85C02-4CC3-FF3E-06D7-7F87EEECCF3A". }, # pos 20 { - kivi_datev_name => 'not yet implemented', + kivi_datev_name => 'beleginfo_art_1', + csv_header_name => 'Beleginfo - Art 1', + max_length => 20, + type => 'Text', + default => 'Artikelbeschreibung', + formatter => sub { return 'Artikelbeschreibung'; }, # make sure nobody ever changes this field through code }, { - kivi_datev_name => 'not yet implemented', + kivi_datev_name => 'description', + csv_header_name => 'Beleginfo - Inhalt 1', + max_length => 210, + type => 'Text', + default => '', }, { - kivi_datev_name => 'not yet implemented', + kivi_datev_name => 'beleginfo_art_2', + csv_header_name => 'Beleginfo - Art 2', + max_length => 20, + type => 'Text', + default => 'Menge', + formatter => sub { return 'Menge'; }, # make sure nobody ever changes this field through code }, { - kivi_datev_name => 'not yet implemented', - }, + kivi_datev_name => 'quantity', + csv_header_name => 'Beleginfo - Inhalt 2', + max_length => 210, + type => 'Value', + formatter => \&_format_amount, + default => '', + }, # pos 24 { kivi_datev_name => 'not yet implemented', }, From a10d06b6b73dd7d28e2dc40f32655788d5f0dff7 Mon Sep 17 00:00:00 2001 From: Johannes Grassler Date: Wed, 6 Mar 2024 15:30:17 +0000 Subject: [PATCH 3/4] DATEV-Export: Metadaten im Buchungstext Nicht jede DATEV-Version und jedes Steuerberatungsbuero kann die Beleginfo-Felder ohne weiteres importieren. Kleinster gemeinsamer Nenner ist das Feld "Buchungstext", in dem nur 60 Zeichen Platz sind. Dieser Commit haengt die Menge (soweit nicht 1) und eine auf die verbleibende Laenge abgeschnittene Beschreibung an das Feld Buchungstext an. (cherry picked from commit c17239966d4c162d62d769429e571e953682bc02) --- SL/DATEV.pm | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/SL/DATEV.pm b/SL/DATEV.pm index 032dcfe7b7..8e1a056344 100644 --- a/SL/DATEV.pm +++ b/SL/DATEV.pm @@ -874,7 +874,22 @@ sub generate_datev_lines { $datev_data{kost2} = $transaction->[$haben]->{'projectdescription'}; if ($transaction->[$haben]->{'name'} ne "") { - $datev_data{buchungstext} = $transaction->[$haben]->{'name'}; + if ( $datev_data{'description'} ) { + my $len = 60; + + $buchungstext = substr($transaction->[$haben]->{'name'}, 0, 18); + + if ( $datev_data{quantity} and $datev_data{quantity} != 1 ) { + $buchungstext .= sprintf("%gx ", $datev_data{quantity}); + } + $buchungstext .= ";" ; + $len = $len - length($buchungstext); + $buchungstext .= substr($datev_data{description}, 0, $len-1); + $datev_data{buchungstext} = $buchungstext; + } else { + $datev_data{buchungstext} = $transaction->[$haben]->{'name'}; + } + } if (($transaction->[$haben]->{'ustid'} // '') ne "") { $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'}); From a1a69a0f1b72f35c095c1295b5b72103c7aed791 Mon Sep 17 00:00:00 2001 From: Johannes Grassler Date: Wed, 6 Mar 2024 17:18:11 +0000 Subject: [PATCH 4/4] DATEV-Export: Kurzname im Buchungstext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lieferanten- oder Kundennamen in voller Länge inklusive Rechtsform können beim DATEV-Export im auf 62 Zeichen limitierten Feld `Buchungstext` eine Menge Platz verschwenden. Dies ist insbesondere der Fall wenn im Buchungstext noch eine Beschreibung des Buchungspostens (siehe vorheriger Commit) steht. Um dort Platz zu sparen führt dieser Commit einen Kurznamen für Lieferanten und Kunden ein, der nur für den DATEV-Export verwendet wird. (cherry picked from commit 03e691037033a62ceebbdc5e3a32e00df09d85df) --- SL/DATEV.pm | 30 ++++++++++++++----- SL/DB/MetaSetup/Customer.pm | 1 + SL/DB/MetaSetup/Vendor.pm | 1 + locale/de/all | 1 + .../customer_vendor_add_shortname.sql | 4 +++ .../customer_vendor/tabs/billing.html | 6 ++++ 6 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 sql/Pg-upgrade2/customer_vendor_add_shortname.sql diff --git a/SL/DATEV.pm b/SL/DATEV.pm index 8e1a056344..6c74b5873d 100644 --- a/SL/DATEV.pm +++ b/SL/DATEV.pm @@ -825,6 +825,7 @@ sub generate_datev_lines { my $datevautomatik = 0; my $taxkey = 0; my $charttax = 0; + my $shortname = ""; my $ustid =""; my ($haben, $soll); for (my $i = 0; $i < $trans_lines; $i++) { @@ -858,6 +859,17 @@ sub generate_datev_lines { if ($transaction->[$i]->{'ivqty'}) { $datev_data{quantity} = $transaction->[$i]->{'ivqty'}; } + if ($transaction->[$i]->{'vendor_id'}) { + my $res = SL::DB::Manager::Vendor->get_first( + where => [id => $transaction->[$i]->{'vendor_id'}] + ); + $shortname = ref $res eq 'SL::DB::Vendor' ? $res->shortname : ""; + } elsif ($transaction->[$i]->{'customer_id'}) { + my $res = SL::DB::Manager::Customer->get_first( + where => [id => $transaction->[$i]->{'customer_id'}] + ); + $shortname = ref $res eq 'SL::DB::Customer' ? $res->shortname : ""; + } } if ($trans_lines >= 2) { @@ -873,24 +885,28 @@ sub generate_datev_lines { $datev_data{kost1} = $transaction->[$haben]->{'departmentdescription'}; $datev_data{kost2} = $transaction->[$haben]->{'projectdescription'}; - if ($transaction->[$haben]->{'name'} ne "") { + if ( ($transaction->[$haben]->{'name'} ne "") or ($shortname ne "") ) { + $buchungstext = $shortname ? $shortname : $transaction->[$haben]->{'name'}; + $buchungstext =~ s/\s*$//g; + if ( $datev_data{'description'} ) { my $len = 60; - $buchungstext = substr($transaction->[$haben]->{'name'}, 0, 18); + # Enforce length constraint on vendor/customer name + $buchungstext = (length($buchungstext) > 20) ? substr($buchungstext, 0, 19) : $buchungstext; + $buchungstext .= "|" ; if ( $datev_data{quantity} and $datev_data{quantity} != 1 ) { - $buchungstext .= sprintf("%gx ", $datev_data{quantity}); + $buchungstext .= sprintf("%g", $datev_data{quantity}); } - $buchungstext .= ";" ; + $buchungstext .= "|" ; $len = $len - length($buchungstext); $buchungstext .= substr($datev_data{description}, 0, $len-1); - $datev_data{buchungstext} = $buchungstext; - } else { - $datev_data{buchungstext} = $transaction->[$haben]->{'name'}; } + $datev_data{buchungstext} = $buchungstext; } + if (($transaction->[$haben]->{'ustid'} // '') ne "") { $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'}); } diff --git a/SL/DB/MetaSetup/Customer.pm b/SL/DB/MetaSetup/Customer.pm index 6bd0abb66b..5ad708ce3c 100644 --- a/SL/DB/MetaSetup/Customer.pm +++ b/SL/DB/MetaSetup/Customer.pm @@ -61,6 +61,7 @@ __PACKAGE__->meta->columns( postal_invoice => { type => 'boolean', default => 'false' }, pricegroup_id => { type => 'integer' }, salesman_id => { type => 'integer' }, + shortname => { type => 'character', length => 20 }, street => { type => 'text' }, taxincluded => { type => 'boolean' }, taxincluded_checked => { type => 'boolean' }, diff --git a/SL/DB/MetaSetup/Vendor.pm b/SL/DB/MetaSetup/Vendor.pm index a4578028b4..fae32e3ae3 100644 --- a/SL/DB/MetaSetup/Vendor.pm +++ b/SL/DB/MetaSetup/Vendor.pm @@ -45,6 +45,7 @@ __PACKAGE__->meta->columns( payment_id => { type => 'integer' }, phone => { type => 'text' }, salesman_id => { type => 'integer' }, + shortname => { type => 'character', length => 20 }, street => { type => 'text' }, taxincluded => { type => 'boolean' }, taxnumber => { type => 'text' }, diff --git a/locale/de/all b/locale/de/all index 35a97ce67c..03c67a2cf1 100644 --- a/locale/de/all +++ b/locale/de/all @@ -3628,6 +3628,7 @@ $ ./scripts/installation_check.pl', 'Shoporders' => 'Shopbest.', 'Shops' => 'Webshops', 'Short' => 'Knapp', + 'Short name for DATEV export' => 'Kurzbezeichnung für DATEV-Export', 'Short onhand' => 'Geringe Lagermenge', 'Short onhand Ordered' => 'Geringer Lagebestand, aber bestellt', 'Should VAT ID or taxnumber be unique for all vendors? This is checked when saving a vendor\'s master data. One of the fields is sufficient and required.' => 'Soll die UStID oder Steuernummer eindeutig über alle Lieferanten sein? Dies wird beim Speichern von Lieferantenstammdaten geprüft. Eines dieser Felder reicht aus und muss angegeben sein.', diff --git a/sql/Pg-upgrade2/customer_vendor_add_shortname.sql b/sql/Pg-upgrade2/customer_vendor_add_shortname.sql new file mode 100644 index 0000000000..d7fdce43d7 --- /dev/null +++ b/sql/Pg-upgrade2/customer_vendor_add_shortname.sql @@ -0,0 +1,4 @@ +-- @tag: customer_vendor_addd_shortname +-- @description: Add a short name for DATEV export to customer and vendor tables +ALTER TABLE customer ADD COLUMN shortname character(20); +ALTER TABLE vendor ADD COLUMN shortname character(20); diff --git a/templates/webpages/customer_vendor/tabs/billing.html b/templates/webpages/customer_vendor/tabs/billing.html index db99b01d55..6b7df0d437 100644 --- a/templates/webpages/customer_vendor/tabs/billing.html +++ b/templates/webpages/customer_vendor/tabs/billing.html @@ -80,6 +80,12 @@ + + + +
[% 'Account' | $T8 %][% 'Account' | $T8 %][% 'Description' | $T8 %][% 'Quantity' | $T8 %][% 'Part Number' | $T8 %] [% 'Amount' | $T8 %] [% 'Tax' | $T8 %] [% 'Taxkey' | $T8 %]
[% SET selected_chart_id = "AP_amount_chart_id_"_ i %] - [% P.chart.picker("AP_amount_chart_id_" _ i, $selected_chart_id, style="width: 400px", type="AP_amount", invalid=0, disabled=readonly, class=(initial_focus == 'row_' _ i ? "initial_focus" : "")) %] + [% P.chart.picker("AP_amount_chart_id_" _ i, $selected_chart_id, style="width: 200px", type="AP_amount", invalid=0, disabled=readonly, class=(initial_focus == 'row_' _ i ? "initial_focus" : "")) %] [% L.hidden_tag("previous_AP_amount_chart_id_" _ i, $selected_chart_id) %] + + + + + + + + Lieferant
+ [% tmp_no = "partno_"_ i %] + [% tmp_id = "parts_id_"_ i %] + + [% IF $tmp_id %] + [% $tmp_no %] + [% ELSE %] + intern + [% END %] +
[% temp = 'selected_taxchart_'_ i %] [% taxcharts = 'taxcharts_' _ i %] - [% L.select_tag('taxchart_'_ i, $taxcharts, value_title_sub = \taxchart_value_title_sub, default = $temp, style="width: 250px") %] + [% L.select_tag('taxchart_'_ i, $taxcharts, value_title_sub = \taxchart_value_title_sub, default = $temp, style="width: 90%") %] [% temp = "project_id_"_ i %] @@ -266,7 +289,7 @@

- [% P.chart.picker('AP_chart_id', AP_chart_id, style="width: 400px", type="AP") %] + [% P.chart.picker('AP_chart_id', AP_chart_id, style="width: 200px", type="AP") %] [% invtotal | html %]
[% 'Short name for DATEV export' | $T8 %] + [% L.input_tag('cv.shortname', SELF.cv.shortname, size = 20) %] +
[% 'Department' | $T8 %]