Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision c7a5335e

Von Sven Schöling vor 4 Monaten hinzugefügt

  • ID c7a5335efe82481e2365a6fb7f0625e820cf040a
  • Vorgänger a371de77
  • Nachfolger b099c63c

fx_transactions: test cases basierend auf #563

t/fxtransaction/ap_transactions.t enthält ein Mini-Framework für
bin/mozilla Integration-Tests. Grobes Muster:

  1. simuliert einen call zu $script.pl?action=$action + form_params
    my ($out, $err, @ret) = make_request($script, \%form_params, $action);
  1. liest sucht alle <input> und <select> elemente die in der html ausgabe # vorkommen
    %form = form_from_html($out);

Wird hier benutzt um simple Flows durch das programm zu simulieren.

Unterschiede anzeigen:

SL/InstallationCheck.pm
100 100
  { name => "URI::Find",                           url => "http://search.cpan.org/~mschwern/",  debian => 'liburi-find-perl' },
101 101
  { name => "GD",              version => '2.00',  url => "http://search.cpan.org/~lds/",       debian => 'libgd-perl' },
102 102
  { name => "Rose::DB::Object", version => 0.809,  url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-object-perl' },
103

  
104
  # first available in debian trixie and ubuntu 23.10
105
  { name => "HTML::Query",                         url => "http://search.cpan.org/~kamelkev/",  debian => 'libhtml-query-perl' },
103 106
);
104 107

  
105 108
$_->{fullname} = join ' ', grep $_, @$_{qw(name version)}
t/fxtransaction/README.txt
1
These tests are for handling of foreign currencies in the old bin/mozilla controllers.
2

  
3
At the time of this writing, the problems are a combination of the following properties:
4

  
5

  
6
1. The old controllers accumulate data on each roundtrip, and user intend often gets lost before even saving, especially with:
7
  - tax included
8
  - foreign exchange
9
  - discounts/price
10
  - payment details
11

  
12
  These information must be processed and rendered both when coming from the user (freshly input) and when coming from the database (when loading a posted invoice)
13

  
14
2. payment specifically has 3 paths that can create them:
15
  - directly in the bin/mozilla form
16
  - via bank import
17
  - with the new Payment helper
18

  
19
3. fx transactions are booked internally in the system currency, but displayed in the frontend in the foreign currency.
20

  
21
4. exchangerates are saved independantly for post date and payment date, which can lead to gains/losses when converting to system currency
22

  
23
   example
24
     user buys for 1000$ (system currency €, exchangerate 1€ - 2$):
25
     purchase invoice shows 1000$, but gets posted as 500€
26
     customer pays 800$, which would be 80% of the invoice, 400€ at the initial exchangerate
27
     but exchangerate at payment date is now 1€ = 1$, so the payment ist actually 800€
28
     invoice still gets 400€ paid. the other 400€ get put into losses  from exchangerates
29

  
30

  
31
5. a special case arises for invoices with foreign currency where the payment is exchanged by the bank and then imported in default currency into kivitendo:
32

  
33

  
34
For a normal purchase invoice with foreign currency:
35

  
36
  1. main transaction (in system currency)
37
    - -500 chart
38
    - +500 Verb.a.L.u.L.
39
  2. payment proportional to stated value in foreign currency converted into system currency: 800$ ist 80% of 1000$, so 80% of 500€ get cleared
40
    - -400 Verb.a.L.u.L. - payment clears 80% of the total amount
41
    - +800 Kasse         - the stated amount in dollar, which happens to also be the euro amount
42
    - -400 fxloss (fx_trasnaction) - the difference is lost
43

  
44
With an exchangerate of 1€ = 1.6$ (500€ = 800$) at payment time, the listing would be:
45

  
46
  1. main transaction (in system currency)
47
    - -500 chart
48
    - +500 Verb.a.L.u.L.
49
  2. payment proportional to stated value in foreign currency converted into system currency: 800$ ist 80% of 1000$, so 80% of 500€ get cleared
50
    - -400 Verb.a.L.u.L. - payment clears 80% of the total amount
51
    - +800 Kasse         - the stated amount in dollar
52
    - -300 Kasse (fx_transaction) - to bring the amount down to the real 500€
53
    - -100 fxloss        - aghain, the difference is lost
54

  
55
For a bank transaction the payment is instead exchanged _before_ being added into the purchase invoice, so there is no fx_transaction
56

  
57
  1. main transaction (in system currency)
58
    - -500 chart
59
    - +500 Verb.a.L.u.L
60
  2. payment proportional to stated value in foreign currency converted into system currency + fx correction
61
    - -400 Verb.a.L.u.L.
62
    - +400 Kasse
63

  
64
in this case there are no fx_transctions.
65

  
66

  
67
6. Old controllers now need to display both of these cases as they find them in the database and preserve them through update(), post() and post_payment()
68

  
69

  
70

  
t/fxtransaction/ap_transactions.t
1
use strict;
2

  
3
use lib 't';
4

  
5
use Test::More;
6

  
7
use SL::Controller::BankTransaction;
8
use SL::DB::AccTransaction;
9
use SL::DB::BankTransaction;
10
use SL::DB::BankTransactionAccTrans;
11
use SL::DB::Chart;
12
use SL::DB::Currency;
13
use SL::DB::Employee;
14
use SL::DB::Exchangerate;
15
use SL::DB::InvoiceItem;
16
use SL::DB::PurchaseInvoice;
17
use SL::DB::TaxZone;
18
use SL::DB::ValidityToken;
19
use SL::Dev::CustomerVendor qw(:ALL);
20
use SL::Dev::Part qw(:ALL);
21
use SL::Dev::Payment qw(:ALL);
22
use SL::Form;
23
use SL::Helper::Number qw(_format_number);
24
use SL::InstanceConfiguration;
25

  
26
use HTML::Query;
27
use Support::TestSetup;
28

  
29
my $part;
30
my $vendor;
31
my $usd;
32
my $taxzone;
33
my $fxgain;
34
my $fxloss;
35
my $payment_terms;
36
my $dt;
37

  
38
sub reset_db {
39
  SL::DB::Manager::BankTransactionAccTrans->delete_all(all => 1);
40
  SL::DB::Manager::BankTransaction->delete_all(all => 1);
41
  SL::DB::Manager::AccTransaction->delete_all(all => 1);
42
  SL::DB::Manager::InvoiceItem->delete_all(all => 1);
43
  SL::DB::Manager::PurchaseInvoice->delete_all(all => 1);
44
  SL::DB::Manager::Exchangerate->delete_all(all => 1);
45
  $usd->delete;
46
  $vendor->delete;
47
  $part->delete;
48
  $payment_terms->delete;
49
}
50

  
51
sub init_db {
52
  SL::DB::Manager::Exchangerate->delete_all(all => 1);
53
  $vendor = new_vendor->save;
54
  $part   = new_part->save;
55
  $usd    = SL::DB::Manager::Currency->find_by(name => 'USD') // SL::DB::Currency->new(name => 'USD')->save;
56
  $taxzone = SL::DB::Manager::TaxZone->find_by( description => 'Inland');
57
  $fxgain = SL::DB::Manager::Chart->find_by(accno => '2660');
58
  $fxloss = SL::DB::Manager::Chart->find_by(accno => '2150');
59
  $payment_terms = create_payment_terms();
60
  $dt    = DateTime->new(year => 1999, month => 1, day => 12);
61

  
62

  
63
  SL::DB::Default->get->update_attributes(
64
    fxgain_accno_id => $fxgain->id,
65
    fxloss_accno_id => $fxloss->id,
66
  )->load;
67
  # reload
68
  $::instance_conf = SL::InstanceConfiguration->new;
69

  
70
}
71

  
72
package MockDispatcher {
73
  sub end_request { die "END_OF_REQUEST" }
74
};
75
$::dispatcher = bless { }, "MockDispatcher";
76

  
77
# make a pseudo request to an old bin/mozilla style entry point
78
# captures STDOUT, STDERR and the return values of the called action
79
#
80
# the given form should be set up like it would be in Dispatcher after read_cgi_input
81
sub make_request {
82
  my ($script, $form, $action) = @_;
83

  
84
  my ($out, $err, @ret);
85

  
86
  package main {
87
    local $SIG{__WARN__} = sub {
88
      # ignore spurious warnings, TAP::Harness calls this warnings enabled
89
    };
90

  
91
    require "bin/mozilla/$script.pl";
92

  
93
    open(my $out_fh, '>', \$out) or die;
94
    open(my $err_fh, '>', \$err) or die;
95

  
96
    local *STDOUT = $out_fh;
97
    local *STDERR = $err_fh;
98

  
99
    local $::form = Form->new;
100
    $::form->{$_} = $form->{$_} for keys %$form;
101
    $::form->{script} = $script.'.pl'; # usually set by dispatcher, needed for checks in update_exchangerate
102
    local $ENV{REQUEST_URI} = "http://localhost/$script.pl"; # needed for Form::redirect_header
103

  
104
    no strict "refs";
105
    eval {
106
      no warnings;
107
      @ret = &{ "::$action" }();
108
      1;
109
    } or do { my $err = $@;
110
      die unless $err =~ /^END_OF_REQUEST/;
111
      @ret = (1);
112
    }
113
  }
114
  return ($out, $err, @ret);
115
}
116

  
117
sub form_from_html {
118
  my ($html) = @_;
119
  my $q = HTML::Query->new(text => $html);
120

  
121
  my %form;
122
  for my $input ($q->query('#form input')->get_elements()) {
123
    next if !$input->attr('name') || $input->attr('disabled');
124
    $form{ $input->attr('name') } = $input->attr('value') // "";
125
  }
126
  for my $select ($q->query('#form select')->get_elements()) {
127
    my $name = $select->attr('name');
128
    my ($selected_option) = (
129
      grep({ $_->tag eq 'option' && $_->attr('selected') } $select->content_list),
130
      grep({ $_->tag eq 'option' } $select->content_list)
131
    );
132

  
133
    $form{ $name } = $selected_option->attr('value') // $selected_option->as_text
134
      if $selected_option;
135
  }
136

  
137
  %form;
138
}
139

  
140
######## main test code #######
141

  
142
Support::TestSetup::login();
143
init_db();
144

  
145
{
146
  my $description = "simple purchase invoice";
147
  #   vendor 1
148
  #   part 1
149
  my $currency = 'USD';
150
  my $exchangerate = 2.5;
151
  my $payment_exchangerate = 1.5;
152

  
153
  my %form;
154

  
155
  # make new invoice
156
  my ($out, $err, @ret) = make_request('ir', { type => 'invoice' }, 'add');
157
  is $ret[0], 1, "new purchase invoice";
158
  %form = form_from_html($out);
159

  
160
  # set invnumber and currency
161
  $form{invnumber}    = $description;
162
  $form{currency}     = $currency;
163

  
164
  # update
165
  ($out, $err, @ret) = make_request('ir', \%form, 'update');
166
  is $ret[0], 1, "update purchase invoice with currency";
167
  %form = form_from_html($out);
168

  
169
  # set part and exchangerate
170
  $form{exchangerate} = _format_number($exchangerate, -2);
171
  $form{partnumber_1} = $part->partnumber;
172

  
173
  # update
174
  ($out, $err, @ret) = make_request('ir', \%form, 'update');
175
  is $ret[0], 1, "update purchase invoice with part and exchangerate";
176
  %form = form_from_html($out);
177

  
178
  # now set par, exchangerate and payments - this will cause the part to be loaded with the lastcost translated into USD
179
  $form{paid_1}           = _format_number($part->lastcost / $exchangerate, -2);  # lastcost = 5€ = 2$
180
  $form{exchangerate_1}   = _format_number($payment_exchangerate, -2);
181

  
182
  ($out, $err, @ret) = make_request('ir', \%form, 'post');
183
  is $ret[0], 1, "posting '$description' does not generate error";
184
  warn $err if $err;
185
  ok $out =~ /ir\.pl\?action=edit&id=(\d+)/, "posting '$description' returns redirect to id";
186
  my $id = $1;
187

  
188
  ($out, $err, @ret) = make_request('ir', { id => $id }, 'edit');
189
  is $ret[0], 1, "'$description' did not cause an error";
190
  warn $err if $err;
191

  
192
  my $q = HTML::Query->new(text => $out);
193
  is $q->query('input[name=paid_1]')->size, 1, "out '$description' contains paid_1";
194
  is $q->query('input[name=paid_1]')->first->attr('value'), '2,00', "out '$description' paid_1 is 2,00 (the dollar amount, not the internal 5.00€)";
195

  
196
  is $q->query('#ui-tabs-basic-data tr.invtotal th')->first->as_text, 'Summe', "'$description' - total present";
197
  is $q->query('#ui-tabs-basic-data tr.invtotal td')->first->as_text, '2,38', "'$description' - total should be 2.00 * 1.19 = 2.38";
198
  is $q->query('#ui-tabs-basic-data input[name=oldtotalpaid]')->first->attr('value'), '2', "'$description' - totalpaid should be 2,00 in dollar, not the internal value";
199
};
200

  
201
{
202
  my $description = "ap transaction from redmine #563";
203
  #   20 on 4710 Verpackungsmaterial
204
  #   20 payment on 1000 kasse
205
  my $currency             = 'USD';
206
  my $exchangerate         = 1.1;
207
  my $payment_exchangerate = 1.3;
208
  my $chart                = SL::DB::Manager::Chart->find_by(accno => '4710');
209
  my %form;
210

  
211
  # make new ap transaction
212
  my ($out, $err, @ret) = make_request('ap', { }, 'add');
213
  is $ret[0], 1, "new ap transaction";
214
  %form = form_from_html($out);
215

  
216
  # set chart, amount, currency, invnumber
217
  $form{AP_amount_chart_id_1} = $chart->id;
218
  $form{amount_1}             = 20;
219
  $form{currency}             = 'USD';
220
  $form{invnumber} = $description;
221

  
222
  # make new ap transaction
223
  ($out, $err, @ret) = make_request('ap', \%form, 'update');
224
  is $ret[0], 1, "update ap transaction with currency";
225
  %form = form_from_html($out);
226

  
227
  # add exchangerate and payments
228
  $form{paid_1}                = 20;
229
  $form{exchangerate}    = _format_number($exchangerate, -2);
230
  $form{exchangerate_1}   = _format_number($payment_exchangerate, -2);
231

  
232
  ($out, $err, @ret) = make_request('ap', \%form, 'post');
233
  is $ret[0], 1, "posting '$description' did not cause an error";
234

  
235
  my $invoice = SL::DB::Manager::PurchaseInvoice->find_by(invnumber => $description);
236
  ok $invoice, "posting '$description' can be found in the database";
237

  
238
  ($out, $err, @ret) = make_request('ap', { id => $invoice->id }, 'edit');
239
  is $ret[0], 1, "loading '$description' did not cause an error";
240
  warn $err if $err;
241

  
242
  my $q = HTML::Query->new(text => $out);
243
  is $q->query('input[name=paid_1]')->size, 1, "out '$description' contains paid_1";
244
  is $q->query('input[name=paid_1]')->first->attr('value'), '20,00', "out '$description' paid_1 is 20 (the dollar amount, not the internal amount)";
245
}
246

  
247
{
248
  my $testname     = 'ap_transaction_fx_gain_fees';
249
  my $usd_amount   = 83300;
250
  my $fx_rate      = 2;
251
  my $fx_rate_bank = 1.75;
252
  my $eur_amount   = $usd_amount * $fx_rate;
253

  
254
  my $netamount     = $eur_amount;
255
  my $amount        = $eur_amount;
256
  my $buysell       = 'sell';
257
  my $eur_payment   = $usd_amount * $fx_rate_bank;
258
  my $usd_payment   = $eur_payment / $fx_rate_bank; # for rounding issues
259

  
260
  my $bank              = SL::DB::Manager::Chart->find_by(description => 'Bank');
261
  my $bank_account = SL::DB::Manager::BankAccount->find_by(chart_id => $bank->id) // SL::DB::BankAccount->new(
262
    account_number => '123',
263
    bank_code      => '123',
264
    iban           => '123',
265
    bic            => '123',
266
    bank           => '123',
267
    chart_id       => $bank->id,
268
    name           => $bank->description,
269
  )->save;
270

  
271
  my $ex = SL::DB::Manager::Exchangerate->find_by(currency_id => $usd->id, transdate => $dt)
272
        ||              SL::DB::Exchangerate->new(currency_id => $usd->id, transdate   => $dt);
273
  $ex->update_attributes($buysell => $fx_rate);
274

  
275
  my $ap_chart = SL::DB::Manager::Chart->get_first(query => [ link => 'AP' ], sort_by => 'accno');
276
  my $ap_amount_chart = SL::DB::Manager::Chart->get_first(query => [ link => 'AP_amount' ], sort_by => 'accno');
277
  my $invoice   = SL::DB::PurchaseInvoice->new(
278
    invoice      => 0,
279
    invnumber    => $testname,
280
    amount       => $amount,
281
    netamount    => $netamount,
282
    transdate    => $dt,
283
    taxincluded  => 0,
284
    vendor_id    => $vendor->id,
285
    taxzone_id   => $vendor->taxzone_id,
286
    currency_id  => $usd->id,
287
    transactions => [],
288
    notes        => 'ap_transaction_fx',
289
  );
290
  $invoice->add_ap_amount_row(
291
    amount     => $netamount,
292
    chart      => $ap_amount_chart,
293
    tax_id     => 0,
294
  );
295

  
296
  $invoice->create_ap_row(chart => $ap_chart);
297
  $invoice->save;
298
  my $ap_transaction_fx = $invoice;
299

  
300
  # check exchangerate
301
  is($ap_transaction_fx->currency->name   , 'USD'     , "$testname: USD currency");
302
  is($ap_transaction_fx->get_exchangerate , '2.00000' , "$testname: fx rate record");
303
  my $bt = create_bank_transaction(record        => $ap_transaction_fx,
304
                                   bank_chart_id => $bank->id,
305
                                   transdate     => $dt,
306
                                   valutadate    => $dt,
307
                                   amount        => $eur_payment,
308
                                   exchangerate  => $fx_rate_bank,
309
                                  ) or die "Couldn't create bank_transaction";
310

  
311
  local $::form = Form->new;
312
  $::form->{invoice_ids} = {
313
    $bt->id => [ $ap_transaction_fx->id ]
314
  };
315
  $::form->{"book_fx_bank_fees_" . $bt->id . "_" . $ap_transaction_fx->id}  = 1;
316
  $::form->{"exchangerate_"      . $bt->id . "_" . $ap_transaction_fx->id}  = "1,75"; # will be parsed
317
  $::form->{"currency_id_"       . $bt->id . "_" . $ap_transaction_fx->id}  = $usd->id;
318

  
319
  my ($stdout, $stderr, @result);
320
  {
321
    open(my $out_fh, '>', \$stdout) or die;
322
    open(my $err_fh, '>', \$stderr) or die;
323

  
324
    local *STDOUT = $out_fh;
325
    local *STDERR = $err_fh;
326
    my $bt_controller = SL::Controller::BankTransaction->new;
327
    @result = $bt_controller->action_save_invoices;
328
  };
329
  ok !$stderr, "ap_transaction '$testname' can be booked with BackTransaction controller";
330

  
331
  $invoice = SL::DB::Manager::PurchaseInvoice->find_by(invnumber => $testname);
332

  
333
  # now load with old code
334
  my ($out, $err, @ret) = make_request('ap', { id => $invoice->id }, 'edit');
335
  is $ret[0], 1, "loading '$testname' did not cause an error";
336
  warn $err if $err;
337

  
338
  my $q = HTML::Query->new(text => $out);
339
  is $q->query('input[name=paid_1]')->size, 1, "out '$testname' contains paid_1";
340
  is $q->query('input[name=paid_1]')->first->attr('value'), _format_number($usd_payment, -2), "out '$testname' paid_1 is $usd_payment (the dollar amount, not the internal amount)";
341
}
342

  
343
reset_db();
344
done_testing();
345

  
346
1;
templates/webpages/ir/form_footer.html
74 74
       <table>
75 75

  
76 76
[%- UNLESS taxincluded %]
77
        <tr>
77
        <tr class="invsubtotal">
78 78
         <th align="right">[% 'Subtotal' | $T8 %]</th>
79 79
         <td align="right">[% LxERP.format_amount(invsubtotal, 2) %]</td>
80 80
       </tr>
......
86 86
[% SET rate_ref        = item _ '_rate' %]
87 87
[% SET total_ref       = item _ '_total' %]
88 88
[% SET netto_ref       = item _ '_netto' %]
89
       <tr>
89
       <tr class="invtaxamount">
90 90
        <th align="right">[% 'Including' | $T8 %] [% $description_ref | html %]&nbsp;[% $rate_ref * 100 %]%</th>
91 91
        <td align="right">[% LxERP.format_amount($total_ref, 2) %]</td>
92 92
       </tr>
93 93
  [%- IF taxincluded %]
94
       <tr>
94
       <tr class="invnetamount">
95 95
        <th align="right">[% 'Net amount' | $T8 %]</th>
96 96
        <td align="right">[% LxERP.format_amount($netto_ref, 2) %]</td>
97 97
       </tr>
98 98
  [%- END %]
99 99
[%- END %]
100 100

  
101
        <tr>
101
        <tr class='invtotal'>
102 102
         <th align="right">[% 'Total' | $T8 %]</th>
103 103
         <td align="right">[% LxERP.format_amount(invtotal, 2) %]</td>
104 104
        </tr>

Auch abrufbar als: Unified diff