592134b53a7ac9be6a2219a66c86add65e246668
[kivitendo-erp.git] / t / helper / shipped_qty.t
1 use strict;
2 use Test::More;
3
4 use lib 't';
5 use Support::TestSetup;
6 use Carp;
7 use Test::Exception;
8 use Data::Dumper;
9 use SL::DB::Part;
10 use SL::DB::Inventory;
11 use SL::DB::TransferType;
12 use SL::DB::Order;
13 use SL::DB::DeliveryOrder;
14 use SL::DB::Customer;
15 use SL::DB::Vendor;
16 use SL::DB::RecordLink;
17 use SL::DB::DeliveryOrderItemsStock;
18 use SL::DB::Bin;
19 use SL::WH;
20 use SL::AM;
21 use SL::Dev::ALL qw(:ALL);
22 use SL::Helper::ShippedQty;
23 use DateTime;
24
25 Support::TestSetup::login();
26
27 clear_up();
28
29 my ($customer, $vendor, @parts, $unit);
30
31 $customer = new_customer(name => 'Testkunde'    )->save;
32 $vendor   = new_vendor(  name => 'Testlieferant')->save;
33
34 my $default_sellprice = 10;
35 my $default_lastcost  =  4;
36
37 my ($wh) = create_warehouse_and_bins();
38 my $bin1 = SL::DB::Manager::Bin->find_by(description => "Bin 1");
39 my $bin2 = SL::DB::Manager::Bin->find_by(description => "Bin 2");
40
41 my %part_defaults = (
42     sellprice    => $default_sellprice,
43     warehouse_id => $wh->id,
44     bin_id       => $bin1->id
45 );
46
47 # create 3 parts to be used in test
48 for my $i ( 1 .. 4 ) {
49   new_part( %part_defaults, partnumber => $i, description => "part $i test" )->save;
50 };
51
52 my $part1 = SL::DB::Manager::Part->find_by( partnumber => '1' ) or die;
53 my $part2 = SL::DB::Manager::Part->find_by( partnumber => '2' ) or die;
54 my $part3 = SL::DB::Manager::Part->find_by( partnumber => '3' ) or die;
55 my $part4 = SL::DB::Manager::Part->find_by( partnumber => '4' ) or die;
56
57 my @part_ids; # list of all part_ids to run checks against
58 push( @part_ids, $_->id ) foreach ( $part1, $part2, $part3, $part4 );
59 my %default_transfer_params = ( wh => $wh, bin => $bin1, unit => 'Stck');
60
61
62 # test purchases first, so there is actually stock available when sales is tested
63
64 note("testing purchases, no fill_up");
65
66 $::form->{type} = 'purchase_order';
67 my $purchase_order = create_purchase_order(
68   save       => 1,
69   orderitems => [ create_order_item(part => $part1, qty => 11),
70                   create_order_item(part => $part2, qty => 12),
71                   create_order_item(part => $part3, qty => 13),
72                 ]
73 );
74
75 Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
76 $purchase_order->orderitems;
77
78 local $::instance_conf->data->{shipped_qty_require_stock_out} = 1;
79 SL::Helper::ShippedQty
80   ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
81   ->calculate($purchase_order)
82   ->write_to_objects;
83
84 is($purchase_order->items_sorted->[0]->{shipped_qty}, 0, "first purchase orderitem has no shipped_qty");
85 ok(!$purchase_order->items_sorted->[0]->{delivered},     "first purchase orderitem is not delivered");
86
87 my $purchase_orderitem_part1 = SL::DB::Manager::OrderItem->find_by( parts_id => $part1->id, trans_id => $purchase_order->id);
88
89 is($purchase_orderitem_part1->shipped_qty, 0, "OrderItem shipped_qty method ok");
90
91 is($purchase_order->closed,     0, 'purchase order is open');
92 # set delivered only if the do is also stocked in
93 ok(!$purchase_order->delivered,    'purchase order is not delivered');
94
95 note('converting purchase order to delivery order');
96 # create purchase delivery order from purchase order
97 my $purchase_delivery_order = $purchase_order->convert_to_delivery_order;
98 is($purchase_order->closed,    0, 'purchase order is open');
99 note('purchase order is not general now delivered');
100 ok(!$purchase_order->delivered,   'purchase order is not delivered');
101
102 SL::Helper::ShippedQty
103   ->new(require_stock_out => 0)
104   ->calculate($purchase_order)
105   ->write_to_objects;
106
107 is($purchase_order->items_sorted->[0]->{shipped_qty}, 11, "require_stock_out => 0: first purchase orderitem has shipped_qty");
108 ok($purchase_order->items_sorted->[0]->{delivered},       "require_stock_out => 0: first purchase orderitem is delivered");
109
110 Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
111 $purchase_order->orderitems;
112
113 SL::Helper::ShippedQty
114   ->new(require_stock_out => 1)
115   ->calculate($purchase_order)
116   ->write_to_objects;
117
118 is($purchase_order->items_sorted->[0]->{shipped_qty}, 0,  "require_stock_out => 1: first purchase orderitem has no shipped_qty");
119 ok(!$purchase_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first purchase orderitem is not delivered");
120
121 # ship items from delivery order
122 transfer_purchase_delivery_order($purchase_delivery_order);
123
124 Rose::DB::Object::Helpers::forget_related($purchase_order, 'orderitems');
125 $purchase_order->orderitems;
126
127 SL::Helper::ShippedQty
128   ->new(require_stock_out => 1, keep_matches => 1)  # shouldn't make a difference now after shipping
129   ->calculate($purchase_order)
130   ->write_to_objects;
131
132 is($purchase_order->items_sorted->[0]->{shipped_qty}, 11, "require_stock_out => 1: first purchase orderitem has shipped_qty");
133 ok($purchase_order->items_sorted->[0]->{delivered},       "require_stock_out => 1: first purchase orderitem is delivered");
134
135 my $purchase_orderitem_part2 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $purchase_order->id);
136
137 is($purchase_orderitem_part2->shipped_qty(require_stock_out => 1), 11, "OrderItem shipped_qty from helper ok");
138
139
140 note('testing sales, no fill_up');
141
142 $::form->{type} = 'sales_order';
143 my $sales_order = create_sales_order(
144   save       => 1,
145   orderitems => [ create_order_item(part => $part1, qty => 5),
146                   create_order_item(part => $part2, qty => 6),
147                   create_order_item(part => $part3, qty => 7),
148                 ]
149 );
150
151 Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
152 $sales_order->orderitems;
153
154 SL::Helper::ShippedQty
155   ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
156   ->calculate($sales_order)
157   ->write_to_objects;
158
159 is($sales_order->items_sorted->[0]->{shipped_qty}, 0,  "first sales orderitem has no shipped_qty");
160 ok(!$sales_order->items_sorted->[0]->{delivered},      "first sales orderitem is not delivered");
161
162 my $orderitem_part1 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $sales_order->id);
163 my $orderitem_part2 = SL::DB::Manager::OrderItem->find_by(parts_id => $part2->id, trans_id => $sales_order->id);
164
165 is($orderitem_part1->shipped_qty, 0, "OrderItem shipped_qty method ok");
166
167 # create sales delivery order from sales order
168 my $sales_delivery_order = $sales_order->convert_to_delivery_order;
169
170 SL::Helper::ShippedQty
171   ->new(require_stock_out => 0)
172   ->calculate($sales_order)
173   ->write_to_objects;
174
175 is($sales_order->items_sorted->[0]->{shipped_qty}, 5, "require_stock_out => 0: first sales orderitem has shipped_qty");
176 ok($sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 0: first sales orderitem is delivered");
177
178 Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
179 $sales_order->orderitems;
180
181 SL::Helper::ShippedQty
182   ->new(require_stock_out => 1)
183   ->calculate($sales_order)
184   ->write_to_objects;
185
186 is($sales_order->items_sorted->[0]->{shipped_qty}, 0,  "require_stock_out => 1: first sales orderitem has no shipped_qty");
187 ok(!$sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first sales orderitem is not delivered");
188
189 # ship items from delivery order
190 transfer_sales_delivery_order($sales_delivery_order);
191
192 Rose::DB::Object::Helpers::forget_related($sales_order, 'orderitems');
193 $sales_order->orderitems;
194
195 SL::Helper::ShippedQty
196   ->new(require_stock_out => 1)
197   ->calculate($sales_order)
198   ->write_to_objects;
199
200 is($sales_order->items_sorted->[0]->{shipped_qty}, 5, "require_stock_out => 1: first sales orderitem has no shipped_qty");
201 ok($sales_order->items_sorted->[0]->{delivered},      "require_stock_out => 1: first sales orderitem is not delivered");
202
203 $orderitem_part1 = SL::DB::Manager::OrderItem->find_by(parts_id => $part1->id, trans_id => $sales_order->id);
204
205 is($orderitem_part1->shipped_qty(require_stock_out => 1), 5, "OrderItem shipped_qty from helper ok");
206
207
208 note('misc tests');
209 my $number_of_linked_items = SL::DB::Manager::RecordLink->get_all_count( where => [ from_table => 'orderitems', to_table => 'delivery_order_items' ] );
210 is ($number_of_linked_items , 6, "6 record_links for items, 3 from sales order, 3 from purchase order");
211
212 note('testing optional orderitems');
213
214 my $item_optional = create_order_item(part => $part3, qty => 7, optional => 1);
215 ok($item_optional->{optional},       "optional order item");
216
217 my $sales_order_opt = create_sales_order(
218   save       => 1,
219   orderitems => [ create_order_item(part => $part1, qty => 5),
220                   create_order_item(part => $part2, qty => 6),
221                   $item_optional,
222                 ]
223 );
224
225
226 SL::Helper::ShippedQty
227   ->new(require_stock_out => 1)  # should make no difference while there is no delivery order
228   ->calculate($sales_order_opt)
229   ->write_to_objects;
230
231 is($sales_order_opt->items_sorted->[2]->{shipped_qty}, 0,  "third optional sales orderitem has no shipped_qty");
232 ok(!$sales_order_opt->items_sorted->[2]->{delivered},      "third optional sales orderitem is not delivered");
233 ok($sales_order_opt->items_sorted->[2]->{optional},        "third optional sales orderitem is optional");
234
235 my $orderitem_part3_opt = SL::DB::Manager::OrderItem->find_by(parts_id => $part3->id, trans_id => $sales_order_opt->id);
236 is($orderitem_part3_opt->shipped_qty, 0, "OrderItem shipped_qty method ok");
237
238 # create sales delivery order from sales order
239 my $sales_delivery_order_opt = $sales_order_opt->convert_to_delivery_order;
240 is(scalar @{ $sales_delivery_order_opt->items_sorted }, 3,   "third optional sales delivery orderitem is there");
241
242 # and delete third item
243 my $optional =  SL::DB::Manager::DeliveryOrderItem->find_by(parts_id => $part3->id, delivery_order_id => $sales_delivery_order_opt->id);
244 SL::DB::DeliveryOrderItem->new(id => $optional->id)->delete;
245 $sales_delivery_order_opt->save(cascade => 1);
246 my $new_sales_delivery_order_opt = SL::DB::Manager::DeliveryOrder->find_by(id => $sales_delivery_order_opt->id);
247 is(scalar @{ $new_sales_delivery_order_opt->items_sorted }, 2,   "third optional sales delivery orderitem is undef");
248
249 SL::Helper::ShippedQty
250   ->new(require_stock_out => 0)
251   ->calculate($sales_order_opt)
252   ->write_to_objects;
253
254 is($sales_order_opt->items_sorted->[0]->{shipped_qty}, 5,  "require_stock_out => 0: first sales orderitem has shipped_qty");
255 ok($sales_order_opt->items_sorted->[0]->{delivered},       "require_stock_out => 0: first sales orderitem is delivered");
256 ok($sales_order_opt->items_sorted->[1]->{delivered},       "require_stock_out => 0: second sales orderitem is delivered");
257 ok(!$sales_order_opt->items_sorted->[2]->{delivered},      "require_stock_out => 0: third sales orderitem is NOT delivered");
258 is($sales_order_opt->items_sorted->[2]->{shipped_qty}, 0,  "require_stock_out => 0: third sales orderitem has no shipped_qty");
259 ok($sales_order_opt->{delivered},                          "require_stock_out => 0: order IS delivered");
260
261 clear_up();
262
263 {
264 #  legacy unlinked scenario:
265 #
266 #  order with two positions of the same part, qtys: 5, 3.
267 #  3 linked delivery orders, with positions:
268 #    1:  3 unlinked
269 #    2:  1 linked to 1, 3 linked to 2
270 #    3:  1 linked to 1
271 #
272 #  should be resolved under fill_up as 5/3, but gets resolved as 4/4
273   my $part = new_part()->save;
274   my $order = create_sales_order(
275     orderitems => [
276       create_order_item(part => $part, qty => 5),
277       create_order_item(part => $part, qty => 3),
278     ],
279   )->save;
280   my $do1 = create_sales_delivery_order(
281     orderitems => [
282       create_delivery_order_item(part => $part, qty => 3),
283     ],
284   );
285   my $do2 = create_sales_delivery_order(
286     orderitems => [
287       create_delivery_order_item(part => $part, qty => 1),
288       create_delivery_order_item(part => $part, qty => 3),
289     ],
290   );
291   my $do3 = create_sales_delivery_order(
292     orderitems => [
293       create_delivery_order_item(part => $part, qty => 1),
294     ],
295   );
296   $order->link_to_record($do1);
297   $order->link_to_record($do2);
298   $order->items_sorted->[0]->link_to_record($do2->items_sorted->[0]);
299   $order->items_sorted->[1]->link_to_record($do2->items_sorted->[1]);
300   $order->link_to_record($do3);
301   $order->items_sorted->[0]->link_to_record($do3->items->[0]);
302
303   SL::Helper::ShippedQty
304     ->new(fill_up => 1, require_stock_out => 0)
305     ->calculate($order)
306     ->write_to_objects;
307
308   is $order->items_sorted->[0]->{shipped_qty}, 5, 'unlinked legacy position test 1';
309   is $order->items_sorted->[1]->{shipped_qty}, 3, 'unlinked legacy position test 2';
310
311 }
312
313 {
314 # edge case:
315 #
316 # suppose an order was delivered, and someone removes one item from the delivery order.
317 # make sure the order is then shown as not delivered.
318 #
319   my $sales_order = create_sales_order(
320     save       => 1,
321     orderitems => [ create_order_item(part => new_part()->save, qty => 5),
322                     create_order_item(part => new_part()->save, qty => 6),
323                     create_order_item(part => new_part()->save, qty => 7),
324                   ]
325   );
326   $sales_order->load;
327
328   my $delivery_order = SL::DB::DeliveryOrder->new_from($sales_order);
329   $delivery_order->save;
330
331   $delivery_order->items(@{ $delivery_order->items_sorted }[0..1]);
332   $delivery_order->save;
333
334   SL::Helper::ShippedQty
335     ->new(fill_up => 0, require_stock_out => 0)
336     ->calculate($sales_order)
337     ->write_to_objects;
338
339   ok !$sales_order->delivered, 'after deleting a position from a delivery order, the order is undelivered again';
340 }
341
342 clear_up();
343
344 done_testing;
345
346 sub clear_up {
347   foreach ( qw(Inventory DeliveryOrderItem DeliveryOrder Price OrderItem Order Part Customer Vendor Bin Warehouse) ) {
348     "SL::DB::Manager::${_}"->delete_all(all => 1);
349   }
350 };