X-Git-Url: http://wagnertech.de/gitweb/gitweb.cgi/mfinanz.git/blobdiff_plain/cb3ae7d0dc987ef703542a683a3929e53dcc0057..d8be5cc409de5b3bc34439599b1481201a5a1c2e:/SL/Controller/BankTransaction.pm diff --git a/SL/Controller/BankTransaction.pm b/SL/Controller/BankTransaction.pm index d34021692..2f14193d7 100644 --- a/SL/Controller/BankTransaction.pm +++ b/SL/Controller/BankTransaction.pm @@ -55,7 +55,9 @@ sub action_search { $self->setup_search_action_bar; $self->render('bank_transactions/search', - BANK_ACCOUNTS => $bank_accounts); + BANK_ACCOUNTS => $bank_accounts, + title => t8('Search bank transactions'), + ); } sub action_list_all { @@ -90,50 +92,59 @@ sub gather_bank_transactions_and_proposals { sort_by => $sort_by, limit => 10000, where => [ - amount => {ne => \'invoice_amount'}, + amount => {ne => \'invoice_amount'}, # '} make emacs happy local_bank_account_id => $params{bank_account}->id, cleared => 0, @where ], ); + + my $has_batch_transaction = (grep { $_->is_batch_transaction } @{ $bank_transactions }) ? 1 : undef; + # credit notes have a negative amount, treat differently - my $all_open_ar_invoices = SL::DB::Manager::Invoice ->get_all(where => [ or => [ amount => { gt => \'paid' }, - and => [ type => 'credit_note', - amount => { lt => \'paid' } - ], - ], - ], - with_objects => ['customer','payment_terms']); - - my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor' ,'payment_terms']); - my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $params{bank_account}->chart_id , - 'sepa_export.executed' => 0, 'sepa_export.closed' => 0 ], with_objects => ['sepa_export']); + my $all_open_ar_invoices = SL::DB::Manager::Invoice->get_all(where => [ or => [ amount => { gt => \'paid' }, # '} make emacs happy + and => [ type => 'credit_note', + amount => { lt => \'paid' } # '} make emacs happy + ], + ], + ], + with_objects => ['customer','payment_terms']); + + my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], # '}] make emacs happy + with_objects => ['vendor' ,'payment_terms']); my @all_open_invoices; # filter out invoices with less than 1 cent outstanding push @all_open_invoices, map { $_->{is_ar}=1 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ar_invoices }; push @all_open_invoices, map { $_->{is_ar}=0 ; $_ } grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices }; - my %sepa_exports; - my %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items; - - # first collect sepa export items to open invoices - foreach my $open_invoice (@all_open_invoices){ - $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2); - $open_invoice->{skonto_type} = 'without_skonto'; - foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) { - my $factor = ($_->ar_id == $open_invoice->id ? 1 : -1); - $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2); - - $open_invoice->{skonto_type} = $_->payment_type; - $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ }; - $sepa_exports{$_->sepa_export_id}->{count}++; - $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id; - $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor; - push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice; + + my (%sepa_exports, %sepa_export_items_by_id, $all_open_sepa_export_items); + if ($has_batch_transaction) { + $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $params{bank_account}->chart_id , + 'sepa_export.executed' => 0, + 'sepa_export.closed' => 0 + ], + with_objects => ['sepa_export']); + %sepa_export_items_by_id = partition_by { $_->ar_id || $_->ap_id } @$all_open_sepa_export_items; + + # first collect sepa export items to open invoices + foreach my $open_invoice (@all_open_invoices){ + $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount,2); + $open_invoice->{skonto_type} = 'without_skonto'; + foreach (@{ $sepa_export_items_by_id{ $open_invoice->id } || [] }) { + my $factor = ($_->ar_id == $open_invoice->id ? 1 : -1); + $open_invoice->{realamount} = $::form->format_amount(\%::myconfig,$open_invoice->amount*$factor,2); + + $open_invoice->{skonto_type} = $_->payment_type; + $sepa_exports{$_->sepa_export_id} ||= { count => 0, is_ar => 0, amount => 0, proposed => 0, invoices => [], item => $_ }; + $sepa_exports{$_->sepa_export_id}->{count}++; + $sepa_exports{$_->sepa_export_id}->{is_ar}++ if $_->ar_id == $open_invoice->id; + $sepa_exports{$_->sepa_export_id}->{amount} += $_->amount * $factor; + push @{ $sepa_exports{$_->sepa_export_id}->{invoices} }, $open_invoice; + } } } - # try to match each bank_transaction with each of the possible open invoices # by awarding points my @proposals; @@ -148,7 +159,7 @@ sub gather_bank_transactions_and_proposals { $bt->{remote_name} .= $bt->{remote_name_1} if $bt->{remote_name_1}; - if ( $bt->is_batch_transaction ) { + if ($has_batch_transaction && $bt->is_batch_transaction ) { my $found=0; foreach ( keys %sepa_exports) { if ( abs(($sepa_exports{$_}->{amount} * 1) - ($bt->amount * 1)) < 0.01 ) { @@ -175,9 +186,9 @@ sub gather_bank_transactions_and_proposals { # score is stored in $bt->{agreement} foreach my $open_invoice (@all_open_invoices) { - ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice, - sepa_export_items => $all_open_sepa_export_items, - ); + + ($open_invoice->{agreement}, $open_invoice->{rule_matches}) = $bt->get_agreement_with_invoice($open_invoice); + $open_invoice->{realamount} = $::form->format_amount(\%::myconfig, $open_invoice->amount * ($open_invoice->{is_ar} ? 1 : -1), 2); } @@ -208,6 +219,7 @@ sub gather_bank_transactions_and_proposals { my @otherproposals = grep { ($_->{agreement} >= $proposal_threshold) && (1 == scalar @{ $_->{proposals} }) + && ($_->{proposals}->[0]->forex == 0) # nyi forex invoices for automatic booking } @{ $bank_transactions }; push @proposals, @otherproposals; @@ -243,13 +255,17 @@ sub action_list { sort_dir => $::form->{sort_dir}, ); + my $ui_tab = $::instance_conf->get_no_bank_proposals ? 0 + : scalar(@{ $proposals }) > 0 ? 1 + : 0; + $::request->layout->add_javascripts("kivi.BankTransaction.js"); $self->render('bank_transactions/list', title => t8('Bank transactions MT940'), BANK_TRANSACTIONS => $bank_transactions, PROPOSALS => $proposals, bank_account => $bank_account, - ui_tab => scalar(@{ $proposals }) > 0 ? 1 : 0, + ui_tab => $ui_tab, ); } @@ -274,12 +290,14 @@ sub action_create_invoice { my $templates_ap = SL::DB::Manager::RecordTemplate->get_all( where => [ template_type => 'ap_transaction' ], + sort_by => [ qw(template_name) ], with_objects => [ qw(employee vendor) ], ); my $templates_gl = SL::DB::Manager::RecordTemplate->get_all( query => [ template_type => 'gl_transaction', chart_id => SL::DB::Manager::BankAccount->find_by(id => $self->transaction->local_bank_account_id)->chart_id, ], + sort_by => [ qw(template_name) ], with_objects => [ qw(employee record_template_items) ], ); @@ -293,14 +311,21 @@ sub action_create_invoice { 'filter.fromdate' => $::form->{filter}->{fromdate}, )); - $self->render( - 'bank_transactions/create_invoice', - { layout => 0 }, - title => t8('Create invoice'), - TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl, - TEMPLATES_AP => $templates_ap, - vendor_name => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef, - ); + # if we have exactly one ap match, use this directly + if ($use_vendor_filter && 1 == scalar @{ $templates_ap }) { + $self->redirect_to($self->load_ap_record_template_url($templates_ap->[0])); + + } else { + my $dialog_html = $self->render( + 'bank_transactions/create_invoice', + { layout => 0, output => 0 }, + title => t8('Create invoice'), + TEMPLATES_GL => $use_vendor_filter && @{ $templates_ap } ? undef : $templates_gl, + TEMPLATES_AP => $templates_ap, + vendor_name => $use_vendor_filter && @{ $templates_ap } ? $vendor_of_transaction->name : undef, + ); + $self->js->run('kivi.BankTransaction.show_create_invoice_dialog', $dialog_html)->render; + } } sub action_ajax_payment_suggestion { @@ -416,7 +441,7 @@ sub action_ajax_add_list { my @all_open_invoices = @{ $all_open_ar_invoices }; # add ap invoices, filtering out subcent open amounts - push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.01 } @{ $all_open_ap_invoices }; + push @all_open_invoices, grep { abs($_->amount - $_->paid) >= 0.005 } @{ $all_open_ap_invoices }; @all_open_invoices = sort { $a->id <=> $b->id } @all_open_invoices; @@ -499,8 +524,11 @@ sub save_invoices { push @{ $self->problems }, $self->save_single_bank_transaction( bank_transaction_id => $bank_transaction_id, invoice_ids => $invoice_ids, - sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], - memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], + sources => [ map { $::form->{"sources_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], + memos => [ map { $::form->{"memos_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], + book_fx_bank_fees => [ map { $::form->{"book_fx_bank_fees_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], + currency_ids => [ map { $::form->{"currency_id_${bank_transaction_id}_${_}"} } @{ $invoice_ids } ], + exchangerates => [ map { $::form->parse_amount(\%::myconfig, $::form->{"exchangerate_${bank_transaction_id}_${_}"}) } @{ $invoice_ids } ], ); $count += scalar( @{$invoice_ids} ); } @@ -568,11 +596,14 @@ sub save_single_bank_transaction { } my (@warnings); + my $transit_items_account = SL::DB::Manager::Chart->find_by(id => SL::DB::Default->get->transit_items_chart_id); + my $worker = sub { my $bt_id = $data{bank_transaction_id}; my $sign = $bank_transaction->amount < 0 ? -1 : 1; my $payment_received = $bank_transaction->amount > 0; my $payment_sent = $bank_transaction->amount < 0; + my ($has_negative_record, $has_positive_record); foreach my $invoice_id (@{ $params{invoice_ids} }) { @@ -584,9 +615,22 @@ sub save_single_bank_transaction { message => $::locale->text("The ID #1 is not a valid database ID.", $invoice_id), }; } + $has_positive_record = 1 if $invoice->amount > 0; # invoice + $has_negative_record = 1 if $invoice->amount < 0; # credit_note push @{ $data{invoices} }, $invoice; } + if (ref $transit_items_account eq 'SL::DB::Chart' && $has_positive_record + && scalar @{ $data{invoices} } == 2 && $has_negative_record) { + + $self->_check_and_book_credit_note( + invoices => $data{invoices}, + chart_id => $transit_items_account->id, + bt_id => $bt_id, + transdate => $bank_transaction->valutadate, + transit_chart => $transit_items_account ); + + } if ( $payment_received && any { ( $_->is_sales && ($_->amount < 0)) || (!$_->is_sales && ($_->amount > 0)) @@ -613,8 +657,11 @@ sub save_single_bank_transaction { my $n_invoices = 0; foreach my $invoice (@{ $data{invoices} }) { - my $source = ($data{sources} // [])->[$n_invoices]; - my $memo = ($data{memos} // [])->[$n_invoices]; + my $source = ($data{sources} // [])->[$n_invoices]; + my $memo = ($data{memos} // [])->[$n_invoices]; + my $fx_rate = ($data{exchangerates} // [])->[$n_invoices]; + my $fx_book = ($data{book_fx_bank_fees} // [])->[$n_invoices]; + my $currency_id = ($data{currency_ids} // [])->[$n_invoices]; $n_invoices++ ; # safety check invoice open @@ -631,12 +678,16 @@ sub save_single_bank_transaction { my ($payment_type, $free_skonto_amount); if ( defined $::form->{invoice_skontos}->{"$bt_id"} ) { - $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} }); + $payment_type = shift(@{ $::form->{invoice_skontos}->{"$bt_id"} }) || ''; } else { $payment_type = 'without_skonto'; } - - if ($payment_type eq 'free_skonto') { + # hack payment type use free_skonto for with_fuzzy_skonto + if ($payment_type eq 'with_fuzzy_skonto_pt') { + $free_skonto_amount = abs($invoice->open_amount - abs($bank_transaction->not_assigned_amount)); + die "Invalid state for fuzzy skonto amount" unless $free_skonto_amount > 0; + $payment_type = 'free_skonto'; # we have a valid free_skonto amount, therefore go ... + } elsif ($payment_type eq 'free_skonto') { # parse user input > 0 if ($::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id}) > 0) { $free_skonto_amount = $::form->parse_amount(\%::myconfig, $::form->{"free_skonto_amount"}->{"$bt_id"}{$invoice->id}); @@ -650,26 +701,53 @@ sub save_single_bank_transaction { } # pay invoice # TODO rewrite this: really booked amount should be a return value of Payment.pm + # -> quick and dirty done -> really booked amount is the first element of return array # also this controller shouldnt care about how to calc skonto. we simply delegate the # payment_type to the helper and get the corresponding bank_transaction values back # hotfix to get the signs right - compare absolute values and later set the signs # should be better done elsewhere - changing not_assigned_amount to abs feels seriously bogus - + # default open amount my $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto : $invoice->open_amount; + # if fx calc new open amount with skonto pt and set new exchange rate (default or for bank_transaction) + if ($fx_rate > 0) { + # 1. set new open amount + die "Exchangerate without currency" unless $currency_id; + die "Invoice currency differs from user input currency" unless $currency_id == $invoice->currency->id; + $open_amount = $payment_type eq 'with_skonto_pt' ? $invoice->amount_less_skonto_fx($fx_rate) : $invoice->open_amount_fx($fx_rate); + # 2. set daily default or custom record exchange rate + my $default_rate = $invoice->get_exchangerate_for_bank_transaction($bank_transaction->id); + if (!$default_rate) { # set new daily default + my $buysell = $invoice->is_sales ? 'buy' : 'sell'; + my $ex = SL::DB::Manager::Exchangerate->find_by(currency_id => $currency_id, + transdate => $bank_transaction->valutadate) + || SL::DB::Exchangerate->new(currency_id => $currency_id, + transdate => $bank_transaction->valutadate); + $ex->update_attributes($buysell => $fx_rate); + $bank_transaction->exchangerate(undef); # maybe user reassigned bank_transaction + } elsif ($default_rate != $fx_rate) { # set record (banktransaction) exchangerate + $bank_transaction->exchangerate($fx_rate); # custom rate, will be displayed in ap, ir, is + } elsif (abs($default_rate - $fx_rate) < 0.001) { + # last valid state default rate is (nearly) the same as user input -> do nothing + } else { die "Invalid exchange rate state:" . $default_rate . " " . $fx_rate; } + } # end fx hook + + # open amount is in default currency -> free_skonto is in default currency, no need to change $open_amount = abs($open_amount); $open_amount -= $free_skonto_amount if ($payment_type eq 'free_skonto'); my $not_assigned_amount = abs($bank_transaction->not_assigned_amount); my $amount_for_booking = ($open_amount < $not_assigned_amount) ? $open_amount : $not_assigned_amount; + my $fx_fee_amount = $fx_book && ($open_amount < $not_assigned_amount) ? $not_assigned_amount - $open_amount : 0; my $amount_for_payment = $amount_for_booking; + # add booking amount + # $amount_for_booking # get the right direction for the payment bookings (all amounts < 0 are stornos, credit notes or negative ap) $amount_for_payment *= -1 if $invoice->amount < 0; $free_skonto_amount *= -1 if ($free_skonto_amount && $invoice->amount < 0); # get the right direction for the bank transaction + # sign is simply the sign of amount in bank_transactions: positive for increase and negative for decrease $amount_for_booking *= $sign; - $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking); - # ... and then pay the invoice my @acc_ids = $invoice->pay_invoice(chart_id => $bank_transaction->local_bank_account->chart_id, trans_id => $invoice->id, @@ -678,7 +756,24 @@ sub save_single_bank_transaction { source => $source, memo => $memo, skonto_amount => $free_skonto_amount, + exchangerate => $fx_rate, + fx_book => $fx_book, + fx_fee_amount => $fx_fee_amount, + currency_id => $currency_id, + bt_id => $bt_id, transdate => $bank_transaction->valutadate->to_kivitendo); + # First element is the booked amount for accno bank + my $bank_amount = shift @acc_ids; + + if (!$invoice->forex) { + # die "Invalid state, calculated invoice_amount differs from expected invoice amount" unless (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.001); + $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking); + } else { + die "Invalid state, calculated invoice_amount differs from expected invoice amount: $amount_for_booking <> " . $bank_amount->{return_bank_amount} + unless $fx_book || (abs($bank_amount->{return_bank_amount}) - abs($amount_for_booking) < 0.005); + $bank_transaction->invoice_amount($bank_transaction->invoice_amount + $bank_amount->{return_bank_amount}); + #$bank_transaction->invoice_amount($bank_transaction->invoice_amount + $amount_for_booking); + } # ... and record the origin via BankTransactionAccTrans if (scalar(@acc_ids) < 2) { return { @@ -792,16 +887,25 @@ sub action_unlink_bank_transaction { my $query = qq|UPDATE $type SET paid = (SELECT COALESCE(abs(sum(amount)),0) FROM acc_trans WHERE trans_id = ? - AND chart_link ilike '%paid%') - WHERE id = ?|; + AND (chart_link ilike '%paid%' + OR chart_id IN (SELECT fxgain_accno_id from defaults) + OR chart_id IN (SELECT fxloss_accno_id from defaults) + ) + ) + WHERE id = ?|; die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id, $trans_id) == -1); + + # undo datepaid if no payment exists + $query = qq|UPDATE $type SET datepaid = null WHERE ID = ? AND paid = 0|; + die if (do_query($::form, $bank_transaction->db->dbh, $query, $trans_id) == -1); } # 4. and delete all (if any) record links my $rl = SL::DB::Manager::RecordLink->delete_all(where => [ from_id => $bt_id, from_table => 'bank_transactions' ]); # 5. finally reset this bank transaction $bank_transaction->invoice_amount(0); + $bank_transaction->exchangerate(undef); $bank_transaction->cleared(0); $bank_transaction->save; # 6. and add a log entry in history_erp @@ -849,6 +953,9 @@ sub make_filter_summary { [ $filter->{"amount:number"}, $::locale->text('Amount') ], [ $filter->{"bank_account_id:integer"}, $::locale->text('Local bank account') ], [ $filter->{"remote_name:substr::ilike"}, $::locale->text('Remote name') ], + [ $filter->{"remote_account_number:substr::ilike"}, $::locale->text('Remote account number') ], + [ $filter->{"remote_bank_code:substr::ilike"} , $::locale->text('Remote bank code') ], + [ $filter->{"purpose:substr::ilike"} , $::locale->text('Purpose') ], ); for (@filters) { @@ -859,17 +966,18 @@ sub make_filter_summary { } sub prepare_report { - my ($self) = @_; + my ($self) = @_; - my $callback = $self->models->get_callback; + my $callback = $self->models->get_callback; - my $report = SL::ReportGenerator->new(\%::myconfig, $::form); - $self->{report} = $report; + my $report = SL::ReportGenerator->new(\%::myconfig, $::form); + $report->{title} = t8('Bank transactions'); + $self->{report} = $report; - my @columns = qw(ids local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose local_account_number local_bank_code id); - my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code); + my @columns = qw(ids local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount invoice_amount invoices currency purpose end_to_end_id local_account_number local_bank_code id); + my @sortable = qw(local_bank_name transdate valudate remote_name remote_account_number remote_bank_code amount purpose local_account_number local_bank_code); - my %column_defs = ( + my %column_defs = ( ids => { raw_header_data => checkbox_tag("", id => "check_all", checkall => "[data-checkall=1]"), 'align' => 'center', raw_data => sub { if (@{ $_[0]->linked_invoices }) { @@ -888,13 +996,23 @@ sub prepare_report { align => 'right' }, invoice_amount => { sub => sub { $_[0]->invoice_amount_as_number }, align => 'right' }, - invoices => { sub => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) { - next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers } }, + invoices => { sub => sub { my @invnumbers; for my $obj (@{ $_[0]->linked_invoices }) { + next unless $obj; push @invnumbers, $obj->invnumber } return \@invnumbers }, + obj_link => sub { my @links; for my $obj (@{ $_[0]->linked_invoices }) { + next unless $obj; my $script = ref $obj eq 'SL::DB::GLTransaction' ? 'gl.pl' + : $obj->is_sales && $obj->invoice ? 'is.pl' + : $obj->is_sales && !$obj->invoice ? 'ar.pl' + : !$obj->is_sales && $obj->invoice ? 'ir.pl' + : !$obj->is_sales && !$obj->invoice ? 'ap.pl' + : die "Invalid invoice state for link"; + push @links,$script . "?action=edit&id=" . $obj->id } return \@links } + }, currency => { sub => sub { $_[0]->currency->name } }, purpose => { }, local_account_number => { sub => sub { $_[0]->local_bank_account->account_number } }, local_bank_code => { sub => sub { $_[0]->local_bank_account->bank_code } }, local_bank_name => { sub => sub { $_[0]->local_bank_account->name } }, + end_to_end_id => { sub => sub { $_[0]->end_to_end_id }, text => $::locale->text('End to end ID') }, id => {}, ); @@ -924,6 +1042,101 @@ sub prepare_report { ); } +sub _check_and_book_credit_note { + my $self = shift; + my %params = @_; + Common::check_params(\%params, qw(chart_id transdate bt_id invoices transit_chart)); + + croak "No invoice " unless (ref $params{invoices}->[0] eq 'SL::DB::PurchaseInvoice') + || (ref $params{invoices}->[0] eq 'SL::DB::Invoice' ); + croak "Not a valid date" unless ref $params{transdate} eq 'DateTime'; + croak "Not a valid chart" unless ref $params{transit_chart} eq 'SL::DB::Chart'; + croak "Need exactly two records" unless scalar @{ $params{invoices} } == 2; + + + my ($has_one_credit_note, $has_one_invoice, $amount, $credit_note_index, $credit_note_no, $invoice_no); + my $index = 0; + foreach my $invoice (@{ $params{invoices} }) { + if ( ( $invoice->is_sales && $invoice->type eq 'credit_note') + || (!$invoice->is_sales && $invoice->invoice_type eq 'purchase_credit_note')) { + # credit_notes | purchase_credit_note + # -1397.11000 | AR | 504.74000 | AP + # 1397.11000 | AR_paid | -504.74000 | AP_paid + + my $mult = $invoice->is_sales ? -1 : 1; # multiplier for getting the right sign for credit_notes + $amount = ($invoice->amount - $invoice->paid) * $mult; + # (-200 - (-10)) * $mult = AR_paid (positive) |AP_paid (negative) + + $has_one_credit_note += 1; + $credit_note_index = $index; + $credit_note_no = $invoice->invnumber; + } else { + $has_one_invoice += 1; + $invoice_no = $invoice->invnumber; + } + $index++; + } + die "Invalid state" unless ($has_one_credit_note == 1 && $has_one_invoice == 1); + + foreach my $invoice (@{ $params{invoices} }) { + my $is_credit_note = $invoice->is_credit_note ? 1 : undef; + my $sign = $invoice->is_credit_note ? 1 : -1; # correct sign for bookings + my $paid_sign = $invoice->is_credit_note ? -1 : 1; # paid is always negative for credit_note + + my $new_acc_trans = SL::DB::AccTransaction->new(trans_id => $invoice->id, + chart_id => $params{transit_chart}->id, + chart_link => $params{transit_chart}->link, + amount => $amount * $sign, + transdate => $params{transdate}, + source => $is_credit_note ? $invoice_no : $credit_note_no, + memo => t8('Automatically assigned with bank transaction'), + taxkey => 0, + tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id); + + my $arap_booking= SL::DB::AccTransaction->new(trans_id => $invoice->id, + chart_id => $invoice->reference_account->id, + chart_link => $invoice->reference_account->link, + amount => $amount * $sign * -1, + transdate => $params{transdate}, + source => '', + taxkey => 0, + tax_id => SL::DB::Manager::Tax->find_by(taxkey => 0)->id); + $new_acc_trans->save; + $arap_booking->save; + $invoice->update_attributes(paid => $invoice->paid + (abs($amount) * $paid_sign), datepaid => $params{transdate}); + + # link both acc_trans transactions + my $id_type = $invoice->is_sales ? 'ar' : 'ap'; + my %props_acc = ( + acc_trans_id => $new_acc_trans->acc_trans_id, + bank_transaction_id => $params{bt_id}, + $id_type => $invoice->id, + ); + SL::DB::BankTransactionAccTrans->new(%props_acc)->save; + %props_acc = ( + acc_trans_id => $arap_booking->acc_trans_id, + bank_transaction_id => $params{bt_id}, + $id_type => $invoice->id, + ); + SL::DB::BankTransactionAccTrans->new(%props_acc)->save; + # done + + # Record a record link from the bank transaction to the credit note + if ($invoice->invoice_type =~ m/credit_note/) { + my %props = ( + from_table => 'bank_transactions', + from_id => $params{bt_id}, + to_table => $id_type, + to_id => $invoice->id, + ); + SL::DB::RecordLink->new(%props)->save; + } + } + # throw away the credit note + splice @{ $params{invoices} }, $credit_note_index, 1; + # and return nothing. hook is completely done +} + sub init_problems { [] } sub init_models { @@ -969,6 +1182,7 @@ sub load_ap_record_template_url { 'form_defaults.paid_1_suggestion' => $::form->format_amount(\%::myconfig, -1 * $self->transaction->amount, 2), 'form_defaults.AP_paid_1_suggestion' => $self->transaction->local_bank_account->chart->accno, 'form_defaults.callback' => $self->callback, + 'form_defaults.notes' => $self->convert_purpose_for_template($template, $self->transaction->purpose), ); } @@ -984,10 +1198,18 @@ sub load_gl_record_template_url { 'form_defaults.callback' => $self->callback, 'form_defaults.bt_id' => $self->transaction->id, 'form_defaults.bt_chart_id' => $self->transaction->local_bank_account->chart->id, - 'form_defaults.description' => $self->transaction->purpose, + 'form_defaults.description' => $self->convert_purpose_for_template($template, $self->transaction->purpose), ); } +sub convert_purpose_for_template { + my ($self, $template, $purpose) = @_; + + # enter custom code here + + return $purpose; +} + sub setup_search_action_bar { my ($self, %params) = @_; @@ -1105,6 +1327,25 @@ GL-records will be deleted completely if a bank transaction was the source. TODO: we still rely on linked_records for the check boxes +=item C + +This method can be used to parse, filter and convert the bank transaction's +purpose string before it will be assigned to the description field of a +gl transaction or to the notes field of an ap transaction. +You have to write your own custom code. + +=item C<_check_and_book_credit_note> + +This method takes a array of invoices with two entries one one valid credit note +and books the amount of the credit note against the invoice via the default +transfer items account (i.e. SKR04 1370) and adds a source and memo entry for the +payment booking. +Logical and visual linking of the payment booking and credit note record to the bank +transaction will also be done (necessary cond. for unlinking a bank transaction). +If the methods success the credit note will be deleted from +the original caller's array and he can further process the data without pondering +about the removed credit note data. + =back =head1 AUTHOR