Merge pull request #37 from kivitendo/2021-delivery-order-controller-8
[kivitendo-erp.git] / t / db_helper / record_links.t
1 use Test::More tests => 66;
2
3 use strict;
4
5 use lib 't';
6 use utf8;
7
8 use Carp;
9 use Data::Dumper;
10 use Support::TestSetup;
11 use Test::Exception;
12 use Test::Deep qw(cmp_bag);
13 use List::Util qw(max);
14
15 use SL::DB::Buchungsgruppe;
16 use SL::DB::Currency;
17 use SL::DB::Customer;
18 use SL::DB::Employee;
19 use SL::DB::Invoice;
20 use SL::DB::Order;
21 use SL::DB::DeliveryOrder;
22 use SL::DB::DeliveryOrder::TypeData qw(:types);
23 use SL::DB::Part;
24 use SL::DB::Unit;
25 use SL::DB::TaxZone;
26
27 my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor, $taxzone);
28 my ($link, $links, $o1, $o2, $d, $i);
29
30 sub clear_up {
31   SL::DB::Manager::DeliveryOrder->delete_all(all => 1);
32   SL::DB::Manager::Order->delete_all(all => 1);
33   SL::DB::Manager::Invoice->delete_all(all => 1);
34   SL::DB::Manager::Part->delete_all(all => 1);
35   SL::DB::Manager::Customer->delete_all(all => 1);
36   SL::DB::Manager::Vendor->delete_all(all => 1);
37 };
38
39 sub reset_state {
40   my %params = @_;
41
42   $params{$_} ||= {} for qw(buchungsgruppe unit customer part tax);
43
44   clear_up();
45
46   $buchungsgruppe  = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group";
47   $employee        = SL::DB::Manager::Employee->current                                                                    || croak "No employee";
48   $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland')                                           || croak "No taxzone";
49
50   $currency_id     = $::instance_conf->get_currency_id;
51
52   $customer     = SL::DB::Customer->new(
53     name        => 'Test Customer',
54     currency_id => $currency_id,
55     taxzone_id  => $taxzone->id,
56     %{ $params{customer} }
57   )->save;
58
59   $vendor     = SL::DB::Vendor->new(
60     name        => 'Test Vendor',
61     currency_id => $currency_id,
62     taxzone_id  => $taxzone->id,
63     %{ $params{vendor} }
64   )->save;
65 }
66
67 sub new_order {
68   my %params  = @_;
69
70   return SL::DB::Order->new(
71     customer_id => $customer->id,
72     currency_id => $currency_id,
73     employee_id => $employee->id,
74     salesman_id => $employee->id,
75     taxzone_id  => $taxzone->id,
76     quotation   => 0,
77     %params,
78   )->save;
79 }
80
81 sub new_delivery_order {
82   my %params  = @_;
83
84   return SL::DB::DeliveryOrder->new(
85     customer_id => $customer->id,
86     currency_id => $currency_id,
87     employee_id => $employee->id,
88     salesman_id => $employee->id,
89     taxzone_id  => $taxzone->id,
90     order_type => SALES_DELIVERY_ORDER_TYPE,
91     %params,
92   )->save;
93 }
94
95 sub new_invoice {
96   my %params  = @_;
97
98   return SL::DB::Invoice->new(
99     customer_id => $customer->id,
100     currency_id => $currency_id,
101     employee_id => $employee->id,
102     salesman_id => $employee->id,
103     gldate      => DateTime->today_local->to_kivitendo,
104     invoice     => 1,
105     taxzone_id  => $taxzone->id,
106     type        => 'invoice',
107     %params,
108   )->save;
109 }
110
111 Support::TestSetup::login();
112
113 reset_state();
114
115 $o1 = new_order();
116 $i  = new_invoice();
117
118 $link = $o1->link_to_record($i);
119
120 # try to add a link
121 is ref $link, 'SL::DB::RecordLink', 'link_to_record returns new link';
122 is $link->from_table, 'oe', 'from_table';
123 is $link->from_id, $o1->id, 'from_id';
124 is $link->to_table, 'ar', 'to_table';
125 is $link->to_id, $i->id, 'to_id';
126
127 # retrieve link
128 $links = $o1->linked_records;
129 is $links->[0]->id, $i->id, 'simple retrieve';
130
131 $links = $o1->linked_records(direction => 'to', to => 'Invoice');
132 is $links->[0]->id, $i->id, 'direct retrieve 1';
133
134 $links = $o1->linked_records(direction => 'to', to => 'SL::DB::Invoice');
135 is $links->[0]->id, $i->id, 'direct retrieve 2 (with SL::DB::)';
136
137 $links = $o1->linked_records(direction => 'to', to => [ 'Invoice', 'Order' ]);
138 is $links->[0]->id, $i->id, 'direct retrieve 3 (array target)';
139
140 $links = $o1->linked_records(direction => 'both', both => 'Invoice');
141 is $links->[0]->id, $i->id, 'direct retrieve 4 (direction both)';
142
143 $links = $i->linked_records(direction => 'from', from => 'Order');
144 is $links->[0]->id, $o1->id, 'direct retrieve 4 (direction from)';
145
146 # what happens if we delete a linked record?
147 $o1->delete;
148
149 $links = $i->linked_records(direction => 'from', from => 'Order');
150 is @$links, 0, 'no dangling link after delete';
151
152 # can we distinguish between types?
153 $o1 = new_order(quotation => 1);
154 $o2 = new_order();
155 $o1->link_to_record($o2);
156
157 $links = $o2->linked_records(direction => 'from', from => 'Order', query => [ quotation => 1 ]);
158 is $links->[0]->id, $o1->id, 'query restricted retrieve 1';
159
160 $links = $o2->linked_records(direction => 'from', from => 'Order', query => [ quotation => 0 ]);
161 is @$links, 0, 'query restricted retrieve 2';
162
163 # try bidirectional linking
164 $o1 = new_order();
165 $o2 = new_order();
166 $o1->link_to_record($o2, bidirectional => 1);
167
168 $links = $o1->linked_records(direction => 'to', to => 'Order');
169 is $links->[0]->id, $o2->id, 'bidi 1';
170 $links = $o1->linked_records(direction => 'from', from => 'Order');
171 is $links->[0]->id, $o2->id, 'bidi 2';
172 $links = $o1->linked_records(direction => 'both', both => 'Order');
173 is $links->[0]->id, $o2->id, 'bidi 3';
174
175 # funky stuff with both
176 #
177 $d = new_delivery_order();
178 $i = new_invoice();
179
180 $o2->link_to_record($d);
181 $d->link_to_record($i);
182 # at this point the structure is:
183 #
184 #   o1 <--> o2 ---> d ---> i
185 #
186
187 $links = $d->linked_records(direction => 'both', to => 'Invoice', from => 'Order', sort_by => 'customer_id', sort_dir => 1);
188 is $links->[0]->id, $o2->id, 'both with different from/to 1';
189 is $links->[1]->id, $i->id,  'both with different from/to 2';
190
191 # what happens if we double link?
192 #
193 $o2->link_to_record($d);
194
195 $links = $o2->linked_records(direction => 'to', to => 'DeliveryOrder');
196 is @$links, 1, 'double link is only added once 1';
197
198 $d->link_to_record($o2, bidirectional => 1);
199 # at this point the structure is:
200 #
201 #   o1 <--> o2 <--> d ---> i
202 #
203
204 $links = $o2->linked_records(direction => 'to', to => 'DeliveryOrder');
205 is @$links, 1, 'double link is only added once 2';
206
207 # doc states that to/from ae optional. test that
208 $links = $o2->linked_records(direction => 'both');
209 is @$links, 2, 'links without from/to get all';
210
211 # doc states you can limit with direction when giving excess params
212 $links = $d->linked_records(direction => 'to', to => 'Invoice', from => 'Order');
213 is $links->[0]->id, $i->id, 'direction to limit params  1';
214 is @$links, 1, 'direction to limit params 2';
215
216 # doc says there will be special values set... lets see
217 $links = $o1->linked_records(direction => 'to', to => 'Order');
218 is $links->[0]->{_record_link_direction}, 'to',  '_record_link_direction to';
219 is $links->[0]->{_record_link}->to_id, $o2->id,  '_record_link to';
220
221 $links = $o1->linked_records(direction => 'from', from => 'Order');
222 is $links->[0]->{_record_link_direction}, 'from',  '_record_link_direction from';
223 is $links->[0]->{_record_link}->to_id, $o1->id,  '_record_link from';
224
225 # check if bidi returns an array of links even if aready existing
226 my @links = $d->link_to_record($o2, bidirectional => 1);
227 # at this point the structure is:
228 #
229 #   o1 <--> o2 <--> d ---> i
230 #
231 is @links, 2, 'bidi returns array of links in array context';
232
233 #  via
234 $links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder');
235 is $links->[0]->id, $i->id,  'simple case via links (string)';
236
237 $links = $o2->linked_records(direction => 'to', to => 'Invoice', via => [ 'DeliveryOrder' ]);
238 is $links->[0]->id, $i->id,  'simple case via links (arrayref)';
239
240 $links = $o1->linked_records(direction => 'to', to => 'Invoice', via => [ 'Order', 'DeliveryOrder' ]);
241 is $links->[0]->id, $i->id,  'simple case via links (2 hops)';
242
243 # multiple links in the same direction from one object
244 $o1->link_to_record($d);
245 # at this point the structure is:
246 #
247 #   o1 <--> o2 <--> d ---> i
248 #     \____________,^
249 #
250
251 $links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder');
252 is $links->[0]->id, $i->id,  'simple case via links (string)';
253
254
255 # o1 must have 2 linked records now:
256 $links = $o1->linked_records(direction => 'to');
257 is @$links, 2,  'more than one link';
258
259 # as a special funny case, o1 via Order, Order will now yield o2, because it bounces back over itself
260 { local $TODO = 'no idea if this is desired';
261 $links = $o2->linked_records(direction => 'to', to => 'Order', via => [ 'Order', 'Order' ]);
262 is @$links, 2,  'via links with bidirectional hop over starting object';
263 }
264
265 # for sorting, get all don't bother with the links, we'll just take our records
266 my @records = ($o2, $i, $o1, $d);
267 my $sorted;
268 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('type', 1, @records);
269 is_deeply $sorted, [$o1, $o2, $d, $i], 'sorting by type';
270 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('type', 0, @records);
271 is_deeply $sorted, [$i, $d, $o2, $o1], 'sorting by type desc';
272
273 $d->donumber(1);
274 $o1->ordnumber(2);
275 $i->invnumber(3);
276 $o2->ordnumber(4);
277
278 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 1, @records);
279 is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting by number';
280 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 0, @records);
281 is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting by number desc';
282
283 # again with natural sorting
284 $d->donumber("a1");
285 $o1->ordnumber("a3");
286 $i->invnumber("a7");
287 $o2->ordnumber("a10");
288
289 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 1, @records);
290 is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting naturally by number';
291 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 0, @records);
292 is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting naturally by number desc';
293
294 $o2->transdate(DateTime->new(year => 2010, month => 3, day => 1));
295 $i->transdate(DateTime->new(year => 2014, month => 3, day => 19));
296 $o1->transdate(DateTime->new(year => 2014, month => 5, day => 1));
297 $d->transdate(DateTime->new(year => 2014, month => 5, day => 2));
298
299 # transdate should be used before itime
300 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('date', 1, @records);
301 is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting by transdate';
302 $sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('date', 0, @records);
303 is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting by transdate desc';
304
305 # now recursive stuff 2, with backlinks
306 $links = $o1->linked_records(direction => 'to', recursive => 1, save_path => 1);
307 is @$links, 4, 'recursive finds all 4 (backlink to self because of bidi o1<->o2)';
308
309 # because of the link o1->d the longest path should be legth 2. test that
310 is max(map { $_->{_record_link_depth} } @$links), 2, 'longest path is 2';
311
312 $links = $o2->linked_records(direction => 'to', recursive => 1);
313 is @$links, 4, 'recursive from o2 finds 4';
314
315 $links = $o1->linked_records(direction => 'from', recursive => 1, save_path => 1);
316 is @$links, 3, 'recursive from o1 finds 3 (not i)';
317
318 $links = $i->linked_records(direction => 'from', recursive => 1, save_path => 1);
319 is @$links, 3, 'recursive from i finds 3 (not i)';
320
321 $links = $o1->linked_records(direction => 'both', recursive => 1, save_path => 1);
322 is @$links, 4, 'recursive dir=both does not give duplicates';
323
324
325 # test batch mode
326 #
327 #
328 #
329
330 reset_state();
331
332 $o1 = new_order();
333 $o2 = new_order();
334 my $i1 = new_invoice();
335 my $i2 = new_invoice();
336
337 $o1->link_to_record($i1);
338 $o2->link_to_record($i2);
339
340 $links = $o1->linked_records(direction => 'to', to => 'Invoice', batch => [ $o1->id, $o2->id ]);
341 is_deeply [ map { $_->id } @$links ], [ $i1->id , $i2->id ], "batch works";
342
343 $links = $o1->linked_records(direction => 'to', recursive => 1, batch => [ $o1->id, $o2->id ]);
344 cmp_bag [ map { $_->id } @$links ], [ $i1->id , $i2->id ], "batch works recursive";
345
346 $links = $o1->linked_records(direction => 'to', to => 'Invoice', batch => [ $o1->id, $o2->id ], by_id => 1);
347 # $::lxdebug->dump(0,  "links", $links);
348 is @{ $links->{$o1->id} }, 1, "batch by_id 1";
349 is @{ $links->{$o2->id} }, 1, "batch by_id 2";
350 is keys %$links, 2, "batch by_id 3";
351 is $links->{$o1->id}[0]->id, $i1->id, "batch by_id 4";
352 is $links->{$o2->id}[0]->id, $i2->id, "batch by_id 5";
353
354 $links = $o1->linked_records(direction => 'to', recursive => 1, batch => [ $o1->id, $o2->id ], by_id => 1);
355 is @{ $links->{$o1->id} }, 1, "batch recursive by_id 1";
356 is @{ $links->{$o2->id} }, 1, "batch recursive by_id 2";
357 is keys %$links, 2, "batch recursive by_id 3";
358 is $links->{$o1->id}[0]->id, $i1->id, "batch recursive by_id 4";
359 is $links->{$o2->id}[0]->id, $i2->id, "batch recursive by_id 5";
360
361 $links = $o1->linked_records(direction => 'both', recursive => 1, batch => [ $o1->id, $o2->id ], by_id => 1);
362 is @{ $links->{$o1->id} }, 1, "batch recursive by_id direction both 1";
363 is @{ $links->{$o2->id} }, 1, "batch recursive by_id direction both 2";
364 is keys %$links, 2, "batch recursive by_id direction both 3";
365 is $links->{$o1->id}[0]->id, $i1->id, "batch recursive by_id direction both 4";
366 is $links->{$o2->id}[0]->id, $i2->id, "batch recursive by_id direction both 5";
367
368 clear_up();
369
370 1;