Revision 964cc586
Von Sven Schöling vor mehr als 10 Jahren hinzugefügt
SL/DB/Helper/LinkedRecords.pm | ||
---|---|---|
193 | 193 |
|
194 | 194 |
my $today = DateTime->today_local; |
195 | 195 |
my $date_xtor = sub { |
196 |
$_[0]->can('transdate_as_date') ? $_[0]->transdate_as_date
|
|
197 |
: $_[0]->can('itime_as_date') ? $_[0]->itime_as_date
|
|
196 |
$_[0]->can('transdate_as_date') ? $_[0]->transdate |
|
197 |
: $_[0]->can('itime_as_date') ? $_[0]->itime->clone->truncate(to => 'day')
|
|
198 | 198 |
: $today; |
199 | 199 |
}; |
200 | 200 |
my $date_comparator = sub { |
... | ... | |
234 | 234 |
|
235 | 235 |
SL::DB::Helper::LinkedRecords - Mixin for retrieving linked records via the table C<record_links> |
236 | 236 |
|
237 |
SYNOPSIS |
|
238 |
|
|
239 |
# In SL::DB::<Object> |
|
240 |
use SL::DB::Helper::LinkedRecords; |
|
241 |
|
|
242 |
# later in consumer code |
|
243 |
# retrieve all links |
|
244 |
my @linked_objects = $order->linked_records( |
|
245 |
direction => 'both', |
|
246 |
); |
|
247 |
|
|
248 |
# only links to Invoices |
|
249 |
my @linked_objects = $order->linked_records( |
|
250 |
direction => 'to', |
|
251 |
to => 'Invoice', |
|
252 |
); |
|
253 |
|
|
254 |
# more than one target |
|
255 |
my @linked_objects = $order->linked_records( |
|
256 |
direction => 'to', |
|
257 |
to => [ 'Invoice', 'Order' ], |
|
258 |
); |
|
259 |
|
|
260 |
# more than one direction |
|
261 |
my @linked_objects = $order->linked_records( |
|
262 |
direction => 'both', |
|
263 |
both => 'Invoice', |
|
264 |
); |
|
265 |
|
|
266 |
# more than one direction and different targets |
|
267 |
my @linked_objects = $order->linked_records( |
|
268 |
direction => 'both', |
|
269 |
to => 'Invoice', |
|
270 |
from => 'Order', |
|
271 |
); |
|
272 |
|
|
273 |
# transitive over known classes |
|
274 |
my @linked_objects = $order->linked_records( |
|
275 |
direction => 'to', |
|
276 |
to => 'Invoice', |
|
277 |
via => 'DeliveryOrder', |
|
278 |
); |
|
279 |
|
|
280 |
# add a new link |
|
281 |
$order->link_to_record($invoice); |
|
282 |
$order->link_to_record($purchase_order, bidirectional => 1); |
|
283 |
|
|
284 |
|
|
237 | 285 |
=head1 FUNCTIONS |
238 | 286 |
|
239 | 287 |
=over 4 |
240 | 288 |
|
241 | 289 |
=item C<linked_records %params> |
242 | 290 |
|
243 |
Retrieves records linked from or to C<$self> via the table |
|
244 |
C<record_links>. The mandatory parameter C<direction> (either C<from>, |
|
245 |
C<to> or C<both>) determines whether the function retrieves records |
|
246 |
that link to C<$self> (for C<direction> = C<to>) or that are linked |
|
247 |
from C<$self> (for C<direction> = C<from>). For C<direction = both> |
|
248 |
all records linked from or to C<$self> are returned. |
|
249 |
|
|
250 |
The optional parameter C<from> or C<to> (same as C<direction>) |
|
251 |
contains the package names of Rose models for table limitation (the |
|
252 |
prefix C<SL::DB::> is optional). It can be a single model name as a |
|
253 |
single scalar or multiple model names in an array reference in which |
|
254 |
case all links matching any of the model names will be returned. |
|
255 |
|
|
256 |
The optional parameter C<via> can be used to retrieve all documents |
|
257 |
that may have intermediate documents inbetween. It is an array |
|
258 |
reference of Rose package names for the models that may be |
|
259 |
intermediate link targets. One example is retrieving all invoices for |
|
260 |
a given quotation no matter whether or not orders and delivery orders |
|
261 |
have been created. If C<via> is given then C<from> or C<to> (depending |
|
262 |
on C<direction>) must be given as well, and it must then not be an |
|
291 |
Retrieves records linked from or to C<$self> via the table C<record_links>. The |
|
292 |
mandatory parameter C<direction> (either C<from>, C<to> or C<both>) determines |
|
293 |
whether the function retrieves records that link to C<$self> (for C<direction> |
|
294 |
= C<to>) or that are linked from C<$self> (for C<direction> = C<from>). For |
|
295 |
C<direction = both> all records linked from or to C<$self> are returned. |
|
296 |
|
|
297 |
The optional parameter C<from> or C<to> (same as C<direction>) contains the |
|
298 |
package names of Rose models for table limitation (the prefix C<SL::DB::> is |
|
299 |
optional). It can be a single model name as a single scalar or multiple model |
|
300 |
names in an array reference in which case all links matching any of the model |
|
301 |
names will be returned. |
|
302 |
|
|
303 |
The optional parameter C<via> can be used to retrieve all documents that may |
|
304 |
have intermediate documents inbetween. It is an array reference of Rose package |
|
305 |
names for the models that may be intermediate link targets. One example is |
|
306 |
retrieving all invoices for a given quotation no matter whether or not orders |
|
307 |
and delivery orders have been created. If C<via> is given then C<from> or C<to> |
|
308 |
(depending on C<direction>) must be given as well, and it must then not be an |
|
263 | 309 |
array reference. |
264 | 310 |
|
265 | 311 |
Examples: |
... | ... | |
267 | 313 |
If you only need invoices created directly from an order C<$order> (no |
268 | 314 |
delivery orders inbetween) then the call could look like this: |
269 | 315 |
|
270 |
my $invoices = $order->linked_records(direction => 'to', |
|
271 |
to => 'Invoice'); |
|
316 |
my $invoices = $order->linked_records( |
|
317 |
direction => 'to', |
|
318 |
to => 'Invoice', |
|
319 |
); |
|
272 | 320 |
|
273 | 321 |
Retrieving all invoices from a quotation no matter whether or not |
274 | 322 |
orders or delivery orders where created: |
275 | 323 |
|
276 |
my $invoices = $quotation->linked_records(direction => 'to', |
|
277 |
to => 'Invoice', |
|
278 |
via => [ 'Order', 'DeliveryOrder' ]); |
|
324 |
my $invoices = $quotation->linked_records( |
|
325 |
direction => 'to', |
|
326 |
to => 'Invoice', |
|
327 |
via => [ 'Order', 'DeliveryOrder' ], |
|
328 |
); |
|
279 | 329 |
|
280 | 330 |
The optional parameter C<query> can be used to limit the records |
281 | 331 |
returned. The following call limits the earlier example to invoices |
282 | 332 |
created today: |
283 | 333 |
|
284 |
my $invoices = $order->linked_records(direction => 'to', |
|
285 |
to => 'Invoice', |
|
286 |
query => [ transdate => DateTime->today_local ]); |
|
334 |
my $invoices = $order->linked_records( |
|
335 |
direction => 'to', |
|
336 |
to => 'Invoice', |
|
337 |
query => [ transdate => DateTime->today_local ], |
|
338 |
); |
|
287 | 339 |
|
288 | 340 |
The optional parameters C<$params{sort_by}> and C<$params{sort_dir}> |
289 | 341 |
can be used in order to sort the result. If C<$params{sort_by}> is |
... | ... | |
329 | 381 |
created with the roles of C<from> and C<to> reversed. This link will |
330 | 382 |
also only be created if it doesn't exist already. |
331 | 383 |
|
332 |
In scalar contenxt returns either the existing link or the newly
|
|
384 |
In scalar context returns either the existing link or the newly |
|
333 | 385 |
created one as an instance of C<SL::DB::RecordLink>. In array context |
334 | 386 |
it returns an array of links (one entry if C<$params{bidirectional}> |
335 | 387 |
is falsish and two entries if it is trueish). |
... | ... | |
358 | 410 |
|
359 | 411 |
=item * C<date> |
360 | 412 |
|
361 |
Sort by the date the record was created or applies to.
|
|
413 |
Sort by the transdate of the record was created or applies to.
|
|
362 | 414 |
|
363 |
=back
|
|
415 |
Note: If the latter has a default setting it will always mask the creation time.
|
|
364 | 416 |
|
365 |
Returns a hash reference.
|
|
417 |
=back
|
|
366 | 418 |
|
367 |
Can be called both as a class or as an instance function.
|
|
419 |
Returns an array reference.
|
|
368 | 420 |
|
369 |
This function is not exported.
|
|
421 |
Can only be called both as a class function since it is noe exported.
|
|
370 | 422 |
|
371 | 423 |
=back |
372 | 424 |
|
t/db_helper/record_links.t | ||
---|---|---|
1 |
use Test::More; |
|
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 |
|
|
13 |
use SL::DB::Buchungsgruppe; |
|
14 |
use SL::DB::Currency; |
|
15 |
use SL::DB::Customer; |
|
16 |
use SL::DB::Employee; |
|
17 |
use SL::DB::Invoice; |
|
18 |
use SL::DB::Order; |
|
19 |
use SL::DB::DeliveryOrder; |
|
20 |
use SL::DB::Part; |
|
21 |
use SL::DB::Unit; |
|
22 |
|
|
23 |
my ($customer, $currency_id, $buchungsgruppe, $employee, $vendor); |
|
24 |
my ($link, $links, $o1, $o2, $d, $i); |
|
25 |
|
|
26 |
sub reset_state { |
|
27 |
my %params = @_; |
|
28 |
|
|
29 |
$params{$_} ||= {} for qw(buchungsgruppe unit customer part tax); |
|
30 |
|
|
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::Customer->delete_all(all => 1); |
|
35 |
SL::DB::Manager::Vendor->delete_all(all => 1); |
|
36 |
|
|
37 |
$buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%', %{ $params{buchungsgruppe} }) || croak "No accounting group"; |
|
38 |
$employee = SL::DB::Manager::Employee->current || croak "No employee"; |
|
39 |
|
|
40 |
$currency_id = $::instance_conf->get_currency_id; |
|
41 |
|
|
42 |
$customer = SL::DB::Customer->new( |
|
43 |
name => 'Test Customer', |
|
44 |
currency_id => $currency_id, |
|
45 |
%{ $params{customer} } |
|
46 |
)->save; |
|
47 |
|
|
48 |
$vendor = SL::DB::Vendor->new( |
|
49 |
name => 'Test Vendor', |
|
50 |
currency_id => $currency_id, |
|
51 |
%{ $params{vendor} } |
|
52 |
)->save; |
|
53 |
} |
|
54 |
|
|
55 |
sub new_order { |
|
56 |
my %params = @_; |
|
57 |
|
|
58 |
return SL::DB::Order->new( |
|
59 |
customer_id => $customer->id, |
|
60 |
currency_id => $currency_id, |
|
61 |
employee_id => $employee->id, |
|
62 |
salesman_id => $employee->id, |
|
63 |
taxzone_id => 0, |
|
64 |
quotation => 0, |
|
65 |
%params, |
|
66 |
)->save; |
|
67 |
} |
|
68 |
|
|
69 |
sub new_delivery_order { |
|
70 |
my %params = @_; |
|
71 |
|
|
72 |
return SL::DB::DeliveryOrder->new( |
|
73 |
customer_id => $customer->id, |
|
74 |
currency_id => $currency_id, |
|
75 |
employee_id => $employee->id, |
|
76 |
salesman_id => $employee->id, |
|
77 |
taxzone_id => 0, |
|
78 |
%params, |
|
79 |
)->save; |
|
80 |
} |
|
81 |
|
|
82 |
sub new_invoice { |
|
83 |
my %params = @_; |
|
84 |
|
|
85 |
return SL::DB::Invoice->new( |
|
86 |
customer_id => $customer->id, |
|
87 |
currency_id => $currency_id, |
|
88 |
employee_id => $employee->id, |
|
89 |
salesman_id => $employee->id, |
|
90 |
gldate => DateTime->today_local->to_kivitendo, |
|
91 |
taxzone_id => 0, |
|
92 |
invoice => 1, |
|
93 |
type => 'invoice', |
|
94 |
%params, |
|
95 |
)->save; |
|
96 |
} |
|
97 |
|
|
98 |
Support::TestSetup::login(); |
|
99 |
|
|
100 |
reset_state(); |
|
101 |
|
|
102 |
|
|
103 |
$o1 = new_order(); |
|
104 |
$i = new_invoice(); |
|
105 |
|
|
106 |
$link = $o1->link_to_record($i); |
|
107 |
|
|
108 |
# try to add a link |
|
109 |
is ref $link, 'SL::DB::RecordLink', 'link_to_record returns new link'; |
|
110 |
is $link->from_table, 'oe', 'from_table'; |
|
111 |
is $link->from_id, $o1->id, 'from_id'; |
|
112 |
is $link->to_table, 'ar', 'to_table'; |
|
113 |
is $link->to_id, $i->id, 'to_id'; |
|
114 |
|
|
115 |
# retrieve link |
|
116 |
$links = $o1->linked_records(direction => 'to', to => 'Invoice'); |
|
117 |
is $links->[0]->id, $i->id, 'direct retrieve 1'; |
|
118 |
|
|
119 |
$links = $o1->linked_records(direction => 'to', to => 'SL::DB::Invoice'); |
|
120 |
is $links->[0]->id, $i->id, 'direct retrieve 2 (with SL::DB::)'; |
|
121 |
|
|
122 |
$links = $o1->linked_records(direction => 'to', to => [ 'Invoice', 'Order' ]); |
|
123 |
is $links->[0]->id, $i->id, 'direct retrieve 3 (array target)'; |
|
124 |
|
|
125 |
$links = $o1->linked_records(direction => 'both', both => 'Invoice'); |
|
126 |
is $links->[0]->id, $i->id, 'direct retrieve 4 (direction both)'; |
|
127 |
|
|
128 |
$links = $i->linked_records(direction => 'from', from => 'Order'); |
|
129 |
is $links->[0]->id, $o1->id, 'direct retrieve 4 (direction from)'; |
|
130 |
|
|
131 |
# what happens if we delete a linked record? |
|
132 |
$o1->delete; |
|
133 |
|
|
134 |
$links = $i->linked_records(direction => 'from', from => 'Order'); |
|
135 |
is @$links, 0, 'no dangling link after delete'; |
|
136 |
|
|
137 |
# can we distinguish between types? |
|
138 |
$o1 = new_order(quotation => 1); |
|
139 |
$o2 = new_order(); |
|
140 |
$o1->link_to_record($o2); |
|
141 |
|
|
142 |
$links = $o2->linked_records(direction => 'from', from => 'Order', query => [ quotation => 1 ]); |
|
143 |
is $links->[0]->id, $o1->id, 'query restricted retrieve 1'; |
|
144 |
|
|
145 |
$links = $o2->linked_records(direction => 'from', from => 'Order', query => [ quotation => 0 ]); |
|
146 |
is @$links, 0, 'query restricted retrieve 2'; |
|
147 |
|
|
148 |
# try bidirectional linking |
|
149 |
$o1 = new_order(); |
|
150 |
$o2 = new_order(); |
|
151 |
$o1->link_to_record($o2, bidirectional => 1); |
|
152 |
|
|
153 |
$links = $o1->linked_records(direction => 'to', to => 'Order'); |
|
154 |
is $links->[0]->id, $o2->id, 'bidi 1'; |
|
155 |
$links = $o1->linked_records(direction => 'from', from => 'Order'); |
|
156 |
is $links->[0]->id, $o2->id, 'bidi 2'; |
|
157 |
$links = $o1->linked_records(direction => 'both', both => 'Order'); |
|
158 |
is $links->[0]->id, $o2->id, 'bidi 3'; |
|
159 |
|
|
160 |
# funky stuff with both |
|
161 |
# |
|
162 |
$d = new_delivery_order(); |
|
163 |
$i = new_invoice(); |
|
164 |
|
|
165 |
$o2->link_to_record($d); |
|
166 |
$d->link_to_record($i); |
|
167 |
|
|
168 |
|
|
169 |
$links = $d->linked_records(direction => 'both', to => 'Invoice', from => 'Order', sort_by => 'customer_id', sort_dir => 1); |
|
170 |
is $links->[0]->id, $o2->id, 'both with different from/to 1'; |
|
171 |
is $links->[1]->id, $i->id, 'both with different from/to 2'; |
|
172 |
|
|
173 |
# what happens if we double link? |
|
174 |
# |
|
175 |
$o2->link_to_record($d); |
|
176 |
|
|
177 |
$links = $o2->linked_records(direction => 'to', to => 'DeliveryOrder'); |
|
178 |
is @$links, 1, 'double link is only added once 1'; |
|
179 |
|
|
180 |
$d->link_to_record($o2, bidirectional => 1); |
|
181 |
|
|
182 |
$links = $o2->linked_records(direction => 'to', to => 'DeliveryOrder'); |
|
183 |
is @$links, 1, 'double link is only added once 2'; |
|
184 |
|
|
185 |
# doc states that to/from ae optional. test that |
|
186 |
$links = $o2->linked_records(direction => 'both'); |
|
187 |
is @$links, 2, 'links without from/to get all'; |
|
188 |
|
|
189 |
# doc says there will be special values set... lets see |
|
190 |
$links = $o1->linked_records(direction => 'to', to => 'Order'); |
|
191 |
is $links->[0]->{_record_link_direction}, 'to', '_record_link_direction to'; |
|
192 |
is $links->[0]->{_record_link}->to_id, $o2->id, '_record_link to'; |
|
193 |
|
|
194 |
$links = $o1->linked_records(direction => 'from', from => 'Order'); |
|
195 |
is $links->[0]->{_record_link_direction}, 'from', '_record_link_direction from'; |
|
196 |
is $links->[0]->{_record_link}->to_id, $o1->id, '_record_link from'; |
|
197 |
|
|
198 |
# check if bidi returns an array of links |
|
199 |
{ local $TODO = 'does not work as advertised'; |
|
200 |
my @links = $d->link_to_record($o2, bidirectional => 1); |
|
201 |
is @links, 2, 'bidi returns array of links in array context'; |
|
202 |
} |
|
203 |
|
|
204 |
# via |
|
205 |
$links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder'); |
|
206 |
is $links->[0]->id, $i->id, 'simple case via links (string)'; |
|
207 |
|
|
208 |
$links = $o2->linked_records(direction => 'to', to => 'Invoice', via => [ 'DeliveryOrder' ]); |
|
209 |
is $links->[0]->id, $i->id, 'simple case via links (arrayref)'; |
|
210 |
|
|
211 |
$links = $o1->linked_records(direction => 'to', to => 'Invoice', via => [ 'Order', 'DeliveryOrder' ]); |
|
212 |
is $links->[0]->id, $i->id, 'simple case via links (2 hops)'; |
|
213 |
|
|
214 |
# multiple links in the same direction from one object |
|
215 |
$o1->link_to_record($d); |
|
216 |
$links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder'); |
|
217 |
is $links->[0]->id, $i->id, 'simple case via links (string)'; |
|
218 |
|
|
219 |
# at this point the structure is: |
|
220 |
# |
|
221 |
# o1 <--> o2 ---> d ---> i |
|
222 |
# \____________,^ |
|
223 |
# |
|
224 |
|
|
225 |
# o1 must have 2 linked records now: |
|
226 |
$links = $o1->linked_records(direction => 'to'); |
|
227 |
is @$links, 2, 'more than one link'; |
|
228 |
|
|
229 |
# as a special funny case, o1 via Order, Order will now yield o2, because it bounces back over itself |
|
230 |
{ local $TODO = 'no idea if this is desired'; |
|
231 |
$links = $o2->linked_records(direction => 'to', to => 'Order', via => [ 'Order', 'Order' ]); |
|
232 |
is @$links, 2, 'via links with bidirectional hop over starting object'; |
|
233 |
} |
|
234 |
|
|
235 |
# for sorting, get all don't bother with the links, we'll just take our records |
|
236 |
my @records = ($o2, $i, $o1, $d); |
|
237 |
my $sorted; |
|
238 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('type', 1, @records); |
|
239 |
is_deeply $sorted, [$o1, $o2, $d, $i], 'sorting by type'; |
|
240 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('type', 0, @records); |
|
241 |
is_deeply $sorted, [$i, $d, $o2, $o1], 'sorting by type desc'; |
|
242 |
|
|
243 |
$d->donumber(1); |
|
244 |
$o1->ordnumber(2); |
|
245 |
$i->invnumber(3); |
|
246 |
$o2->ordnumber(4); |
|
247 |
|
|
248 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 1, @records); |
|
249 |
is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting by number'; |
|
250 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 0, @records); |
|
251 |
is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting by number desc'; |
|
252 |
|
|
253 |
# again with natural sorting |
|
254 |
$d->donumber("a1"); |
|
255 |
$o1->ordnumber("a3"); |
|
256 |
$i->invnumber("a7"); |
|
257 |
$o2->ordnumber("a10"); |
|
258 |
|
|
259 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 1, @records); |
|
260 |
is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting naturally by number'; |
|
261 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('number', 0, @records); |
|
262 |
is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting naturally by number desc'; |
|
263 |
|
|
264 |
$o2->transdate(DateTime->new(year => 2010, month => 3, day => 1)); |
|
265 |
$i->transdate(DateTime->new(year => 2014, month => 3, day => 19)); |
|
266 |
$o1->transdate(DateTime->new(year => 2014, month => 5, day => 1)); |
|
267 |
$d->transdate(DateTime->new(year => 2014, month => 5, day => 2)); |
|
268 |
|
|
269 |
# transdate should be used before itime |
|
270 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('date', 1, @records); |
|
271 |
is_deeply $sorted, [$o2, $i, $o1, $d], 'sorting by transdate'; |
|
272 |
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('date', 0, @records); |
|
273 |
is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting by transdate desc'; |
|
274 |
|
|
275 |
done_testing(); |
|
276 |
|
|
277 |
1; |
Auch abrufbar als: Unified diff
LinkedRecords: Test und Doku Update