1 # This file has been auto-generated only because it didn't exist.
2 # Feel free to modify it at will; it will not be overwritten automatically.
4 package SL::DB::BankTransaction;
8 use SL::DB::MetaSetup::BankTransaction;
9 use SL::DB::Manager::BankTransaction;
10 use SL::DB::Helper::LinkedRecords;
13 require SL::DB::Invoice;
14 require SL::DB::PurchaseInvoice;
16 __PACKAGE__->meta->initialize;
19 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
20 #__PACKAGE__->meta->make_manager_class;
23 my ($self, $other) = @_;
25 return 1 if $self->transdate && !$other->transdate;
26 return -1 if !$self->transdate && $other->transdate;
29 $result = $self->transdate <=> $other->transdate if $self->transdate;
30 return $result || ($self->id <=> $other->id);
36 #my $record_links = $self->linked_records(direction => 'both');
40 my $record_links = SL::DB::Manager::RecordLink->get_all(where => [ from_table => 'bank_transactions', from_id => $self->id ]);
42 foreach my $record_link (@{ $record_links }) {
43 push @linked_invoices, SL::DB::Manager::Invoice->find_by(id => $record_link->to_id) if $record_link->to_table eq 'ar';
44 push @linked_invoices, SL::DB::Manager::PurchaseInvoice->find_by(id => $record_link->to_id) if $record_link->to_table eq 'ap';
45 push @linked_invoices, SL::DB::Manager::GLTransaction->find_by(id => $record_link->to_id) if $record_link->to_table eq 'gl';
48 return [ @linked_invoices ];
51 sub is_batch_transaction {
52 ($_[0]->transaction_code // '') eq "191";
56 sub get_agreement_with_invoice {
57 my ($self, $invoice) = @_;
59 carp "get_agreement_with_invoice needs an invoice object as its first argument"
60 unless ref($invoice) eq 'SL::DB::Invoice' or ref($invoice) eq 'SL::DB::PurchaseInvoice';
63 cust_vend_name_in_purpose => 1,
64 cust_vend_number_in_purpose => 1,
69 datebonus_negative => -1,
70 depositor_matches => 2,
72 exact_open_amount => 4,
73 invoice_in_purpose => 2,
74 own_invoice_in_purpose => 5,
75 invnumber_in_purpose => 1,
76 own_invnumber_in_purpose => 4,
77 # overpayment => -1, # either other invoice is more likely, or several invoices paid at once
78 payment_before_invoice => -2,
79 payment_within_30_days => 1,
80 remote_account_number => 3,
81 skonto_exact_amount => 5,
82 skonto_fuzzy_amount => 3,
84 sepa_export_item => 5,
86 batch_sepa_transaction => 20,
90 my ($agreement,$rule_matches);
92 if ( $self->is_batch_transaction && $self->{sepa_export_ok}) {
93 $agreement += $points{batch_sepa_transaction};
94 $rule_matches .= 'batch_sepa_transaction(' . $points{'batch_sepa_transaction'} . ') ';
97 # check swiss qr reference if feature enabled
98 if ($::instance_conf->get_create_qrbill_invoices == 1) {
99 if ($self->{qr_reference} && $invoice->{qr_reference} &&
100 $self->{qr_reference} eq $invoice->{qr_reference}) {
102 $agreement += $points{qr_reference};
103 $rule_matches .= 'qr_reference(' . $points{'qr_reference'} . ') ';
107 # compare banking arrangements
108 my ($iban, $bank_code, $account_number);
109 $bank_code = $invoice->customer->bank_code if $invoice->is_sales;
110 $account_number = $invoice->customer->account_number if $invoice->is_sales;
111 $iban = $invoice->customer->iban if $invoice->is_sales;
112 $bank_code = $invoice->vendor->bank_code if ! $invoice->is_sales;
113 $iban = $invoice->vendor->iban if ! $invoice->is_sales;
114 $account_number = $invoice->vendor->account_number if ! $invoice->is_sales;
116 # check only valid remote_account_number (with some content)
117 if ($self->remote_account_number) {
118 if ($bank_code eq $self->remote_bank_code && $account_number eq $self->remote_account_number) {
119 $agreement += $points{remote_account_number};
120 $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
121 } elsif ($iban eq $self->remote_account_number) { # elsif -> do not add twice
122 $agreement += $points{remote_account_number};
123 $rule_matches .= 'remote_account_number(' . $points{'remote_account_number'} . ') ';
127 my $datediff = $self->transdate->{utc_rd_days} - $invoice->transdate->{utc_rd_days};
128 $invoice->{datediff} = $datediff;
131 if (abs(abs($invoice->amount) - abs($self->amount)) < 0.01 &&
132 $::form->format_amount(\%::myconfig,abs($invoice->amount),2) eq
133 $::form->format_amount(\%::myconfig,abs($self->amount),2)
135 $agreement += $points{exact_amount};
136 $rule_matches .= 'exact_amount(' . $points{'exact_amount'} . ') ';
139 # compare open amount, preventing double points when open amount = invoice amount
140 if ( $invoice->amount != $invoice->open_amount && abs(abs($invoice->open_amount) - abs($self->amount)) < 0.01 &&
141 $::form->format_amount(\%::myconfig,abs($invoice->open_amount),2) eq
142 $::form->format_amount(\%::myconfig,abs($self->amount),2)
144 $agreement += $points{exact_open_amount};
145 $rule_matches .= 'exact_open_amount(' . $points{'exact_open_amount'} . ') ';
148 if ( $invoice->skonto_date && abs(abs($invoice->amount_less_skonto) - abs($self->amount)) < 0.01 &&
149 $::form->format_amount(\%::myconfig,abs($invoice->amount_less_skonto),2) eq
150 $::form->format_amount(\%::myconfig,abs($self->amount),2)
152 $agreement += $points{skonto_exact_amount};
153 $rule_matches .= 'skonto_exact_amount(' . $points{'skonto_exact_amount'} . ') ';
154 $invoice->{skonto_type} = 'with_skonto_pt';
155 } elsif ( $::instance_conf->get_fuzzy_skonto
156 && $invoice->skonto_date && $::instance_conf->get_fuzzy_skonto_percentage > 0
157 && abs(abs($invoice->amount_less_skonto) - abs($self->amount))
158 < abs($invoice->amount / (100 / $::instance_conf->get_fuzzy_skonto_percentage))) {
159 # we have a skonto within the range of fuzzy skonto percentage (default 0.5%)
160 $agreement += $points{skonto_fuzzy_amount};
161 $rule_matches .= 'skonto_fuzzy_amount(' . $points{'skonto_fuzzy_amount'} . ') ';
162 $invoice->{skonto_type} = 'with_fuzzy_skonto_pt';
165 #search invoice number in purpose
166 my $invnumber = $invoice->invnumber;
167 # invnumber has to have at least 3 characters
168 my $squashed_purpose = $self->purpose;
169 $squashed_purpose =~ s/ //g;
170 if (length($invnumber) > 4 && $squashed_purpose =~ /\Q$invnumber/ && $invoice->is_sales){
171 $agreement += $points{own_invoice_in_purpose};
172 $rule_matches .= 'own_invoice_in_purpose(' . $points{'own_invoice_in_purpose'} . ') ';
173 } elsif (length($invnumber) > 3 && $squashed_purpose =~ /\Q$invnumber/ ) {
174 $agreement += $points{invoice_in_purpose};
175 $rule_matches .= 'invoice_in_purpose(' . $points{'invoice_in_purpose'} . ') ';
177 # only check number part of invoice number
178 $invnumber =~ s/[A-Za-z_]+//g;
179 if (length($invnumber) > 4 && $squashed_purpose =~ /\Q$invnumber/ && $invoice->is_sales){
180 $agreement += $points{own_invnumber_in_purpose};
181 $rule_matches .= 'own_invnumber_in_purpose(' . $points{'own_invnumber_in_purpose'} . ') ';
182 } elsif (length($invnumber) > 3 && $squashed_purpose =~ /\Q$invnumber/ ) {
183 $agreement += $points{invnumber_in_purpose};
184 $rule_matches .= 'invnumber_in_purpose(' . $points{'invnumber_in_purpose'} . ') ';
189 if (( $invoice->is_sales && $invoice->amount > 0 && $self->amount < 0 ) ||
190 ( $invoice->is_sales && $invoice->amount < 0 && $self->amount > 0 ) ) { # sales credit note
191 $agreement += $points{wrong_sign};
192 $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
194 if (( !$invoice->is_sales && $invoice->amount > 0 && $self->amount > 0) ||
195 ( !$invoice->is_sales && $invoice->amount < 0 && $self->amount < 0) ) { # purchase credit note
196 $agreement += $points{wrong_sign};
197 $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') ';
200 # search customer/vendor number in purpose
202 $cvnumber = $invoice->customer->customernumber if $invoice->is_sales;
203 $cvnumber = $invoice->vendor->vendornumber if ! $invoice->is_sales;
204 if ( $cvnumber && $self->purpose =~ /\b$cvnumber\b/i ) {
205 $agreement += $points{cust_vend_number_in_purpose};
206 $rule_matches .= 'cust_vend_number_in_purpose(' . $points{'cust_vend_number_in_purpose'} . ') ';
209 # search for customer/vendor name in purpose (may contain GMBH, CO KG, ...)
211 $cvname = $invoice->customer->name if $invoice->is_sales;
212 $cvname = $invoice->vendor->name if ! $invoice->is_sales;
213 if ( $cvname && $self->purpose =~ /\b\Q$cvname\E\b/i ) {
214 $agreement += $points{cust_vend_name_in_purpose};
215 $rule_matches .= 'cust_vend_name_in_purpose(' . $points{'cust_vend_name_in_purpose'} . ') ';
218 # compare depositorname, don't try to match empty depositors
220 $depositorname = $invoice->customer->depositor if $invoice->is_sales;
221 $depositorname = $invoice->vendor->depositor if ! $invoice->is_sales;
222 if ( $depositorname && $self->remote_name =~ /\Q$depositorname/ ) {
223 $agreement += $points{depositor_matches};
224 $rule_matches .= 'depositor_matches(' . $points{'depositor_matches'} . ') ';
227 #Check if words in remote_name appear in cvname
228 my $check_string_points = _check_string($self->remote_name,$cvname);
229 if ( $check_string_points ) {
230 $agreement += $check_string_points;
231 $rule_matches .= 'remote_name(' . $check_string_points . ') ';
234 # transdate prefilter: compare transdate of bank_transaction with transdate of invoice
235 if ( $datediff < -5 ) { # this might conflict with advance payments
236 $agreement += $points{payment_before_invoice};
237 $rule_matches .= 'payment_before_invoice(' . $points{'payment_before_invoice'} . ') ';
239 if ( $datediff < 30 ) {
240 $agreement += $points{payment_within_30_days};
241 $rule_matches .= 'payment_within_30_days(' . $points{'payment_within_30_days'} . ') ';
244 # only if we already have a good agreement, let date further change value of agreement.
245 # this is so that if there are several plausible open invoices which are all equal
246 # (rent jan, rent feb...) the one with the best date match is chosen over
249 # another way around this is to just pre-filter by periods instead of matching everything
250 if ( $agreement > 5 ) {
251 if ( $datediff == 0 ) {
252 $agreement += $points{datebonus0};
253 $rule_matches .= 'datebonus0(' . $points{'datebonus0'} . ') ';
254 } elsif ( $datediff > 0 and $datediff <= 14 ) {
255 $agreement += $points{datebonus14};
256 $rule_matches .= 'datebonus14(' . $points{'datebonus14'} . ') ';
257 } elsif ( $datediff >14 and $datediff < 35) {
258 $agreement += $points{datebonus35};
259 $rule_matches .= 'datebonus35(' . $points{'datebonus35'} . ') ';
260 } elsif ( $datediff >34 and $datediff < 120) {
261 $agreement += $points{datebonus120};
262 $rule_matches .= 'datebonus120(' . $points{'datebonus120'} . ') ';
263 } elsif ( $datediff < 0 ) {
264 $agreement += $points{datebonus_negative};
265 $rule_matches .= 'datebonus_negative(' . $points{'datebonus_negative'} . ') ';
267 # e.g. datediff > 120
271 my $seis = $invoice->find_sepa_export_items({ executed => 0 });
274 # test if end to end id matches for any sepa export
275 if ($self->end_to_end_id &&
276 grep { $self->end_to_end_id eq $_->end_to_end_id } @{ $seis }) {
277 $agreement += $points{end_to_end_id};
278 $rule_matches .= 'end_to_end_id(' . $points{'end_to_end_id'} . ') ';
281 # if there is exactly one non-executed sepa_export_item for the invoice
282 # test for amount and id matching only, sepa transfer date and bank
283 # transaction date needn't match
284 if (scalar @{ $seis } == 1 && $invoice->id == $seis->[0]->arap_id
285 && abs($self->amount) == $seis->[0]->amount ) {
286 $agreement += $points{sepa_export_item};
287 $rule_matches .= 'sepa_export_item(' . $points{'sepa_export_item'} . ') ';
290 return ($agreement,$rule_matches);
294 my $bankstring = shift;
295 my $namestring = shift;
296 return 0 unless $bankstring and $namestring;
298 my @bankwords = grep(/^\w+$/, split(/\b/,$bankstring));
301 foreach my $bankword ( @bankwords ) {
302 # only try to match strings with more than 2 characters
303 next unless length($bankword)>2;
304 if ( $namestring =~ /\b$bankword\b/i ) {
312 sub not_assigned_amount {
315 my $not_assigned_amount = $self->amount - $self->invoice_amount;
316 die ("undefined state") if (abs($not_assigned_amount) > abs($self->amount));
318 return $not_assigned_amount;
324 # check for closed period
325 croak t8('Illegal date') unless ref $self->valutadate eq 'DateTime';
328 my $closedto = $::locale->parse_date_to_object($::instance_conf->get_closedto);
329 if ( ref $closedto && $self->valutadate < $closedto ) {
343 SL::DB::BankTransaction
349 =item C<get_agreement_with_invoice $invoice>
351 Using a point system this function checks whether the bank transaction matches
352 an invoices, using a variety of tests, such as
358 =item * amount_less_skonto
362 =item * invoice number in purpose
364 =item * customer or vendor name in purpose
366 =item * account number matches account number of customer or vendor
370 The total number of points, and the rules that matched, are returned.
373 my $bt = SL::DB::Manager::BankTransaction->find_by(id => 522);
374 my $invoice = SL::DB::Manager::Invoice->find_by(invnumber => '198');
375 my ($agreement,rule_matches) = $bt->get_agreement_with_invoice($invoice);
377 =item C<linked_invoices>
379 Returns an array of record objects (invoices, debit, credit or gl objects)
380 which are linked for this bank transaction.
382 Returns an empty array ref if no links are found.
384 croak("No linked records at all") unless @{ $bt->linked_invoices() };
387 =item C<not_assigned_amount>
389 Returns the not open amount of this bank transaction.
390 Dies if the return amount is higher than the original amount.
392 =item C<closed_period>
394 Returns 1 if the bank transaction valutadate is in a closed period, 0 if the
395 valutadate of the bank transaction is not in a closed period.
401 G. Richardson E<lt>grichardson@kivitendo-premium.de<gt>