Revision 2f9853c8
Von Tamino Steinert vor etwa 1 Jahr hinzugefügt
SL/Controller/DispositionManager.pm | ||
---|---|---|
1 |
package SL::Controller::DispositionManager; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use parent qw(SL::Controller::Base); |
|
6 |
|
|
7 |
use SL::Controller::Helper::GetModels; |
|
8 |
use SL::Controller::Helper::ReportGenerator; |
|
9 |
use SL::DB::Part; |
|
10 |
use SL::DB::PurchaseBasketItem; |
|
11 |
use SL::DB::Order; |
|
12 |
use SL::DB::OrderItem; |
|
13 |
use SL::DB::Vendor; |
|
14 |
use SL::PriceSource; |
|
15 |
use SL::Locale::String qw(t8); |
|
16 |
use SL::Helper::Flash qw(flash flash_later); |
|
17 |
use SL::DBUtils; |
|
18 |
|
|
19 |
use Data::Dumper; |
|
20 |
|
|
21 |
use Rose::Object::MakeMethods::Generic ( |
|
22 |
'scalar --get_set_init' => [ qw(models) ], |
|
23 |
); |
|
24 |
|
|
25 |
sub action_list_parts { |
|
26 |
my ($self) = @_; |
|
27 |
$self->prepare_report(t8('Reorder Level List'), $::form->{noshow} ? 1 : 0 ); |
|
28 |
|
|
29 |
my $objects = $::form->{noshow} ? [] : $self->models->get; |
|
30 |
|
|
31 |
$self->_setup_list_action_bar; |
|
32 |
$self->report_generator_list_objects( |
|
33 |
report => $self->{report}, objects => $objects); |
|
34 |
} |
|
35 |
|
|
36 |
sub prepare_report { |
|
37 |
my ($self, $title, $noshow ) = @_; |
|
38 |
|
|
39 |
my $report = SL::ReportGenerator->new(\%::myconfig, $::form); |
|
40 |
$self->{report} = $report; |
|
41 |
|
|
42 |
my @columns = qw( |
|
43 |
partnumber description available onhand rop ordered |
|
44 |
); |
|
45 |
my @visible = qw( |
|
46 |
partnumber description available onhand rop ordered |
|
47 |
); |
|
48 |
my @sortable = qw(partnumber description); |
|
49 |
|
|
50 |
my %column_defs = ( |
|
51 |
partnumber => { |
|
52 |
sub => sub { $_[0]->partnumber }, |
|
53 |
text => t8('Part Number'), |
|
54 |
obj_link => sub { $_[0]->presenter->link_to }, |
|
55 |
}, |
|
56 |
description => { |
|
57 |
sub => sub { $_[0]->description }, |
|
58 |
text => t8('Part Description'), |
|
59 |
obj_link => sub { $_[0]->presenter->link_to }, |
|
60 |
}, |
|
61 |
available => { |
|
62 |
sub => sub { $::form->format_amount(\%::myconfig,$_[0]->onhandqty,2); }, |
|
63 |
text => t8('Available Stock'), |
|
64 |
}, |
|
65 |
onhand => { |
|
66 |
sub => sub { $::form->format_amount(\%::myconfig,$_[0]->stockqty,2); }, |
|
67 |
text => t8('Total Stock'), |
|
68 |
}, |
|
69 |
rop => { |
|
70 |
sub => sub { $::form->format_amount(\%::myconfig,$_[0]->rop,2); }, |
|
71 |
text => t8('Rop'), |
|
72 |
}, |
|
73 |
ordered => { |
|
74 |
sub => sub { $::form->format_amount( |
|
75 |
\%::myconfig,$_[0]->get_open_ordered_qty,2); }, |
|
76 |
text => t8('Ordered purchase'), |
|
77 |
}, |
|
78 |
); |
|
79 |
|
|
80 |
map { $column_defs{$_}->{visible} = 1 } @visible; |
|
81 |
|
|
82 |
$report->set_options( |
|
83 |
controller_class => 'DispositionManager', |
|
84 |
output_format => 'HTML', |
|
85 |
title => t8($title), |
|
86 |
allow_pdf_export => 1, |
|
87 |
allow_csv_export => 1, |
|
88 |
no_data_message => !$noshow, |
|
89 |
); |
|
90 |
$report->set_columns(%column_defs); |
|
91 |
$report->set_column_order(@columns); |
|
92 |
$report->set_export_options(qw(list_parts)); |
|
93 |
$report->set_options_from_form; |
|
94 |
|
|
95 |
unless ( $noshow ) { |
|
96 |
if ($report->{options}{output_format} =~ /^(pdf|csv)$/i) { |
|
97 |
$self->models->disable_plugin('paginated'); |
|
98 |
} |
|
99 |
$self->models->finalize; # for filter laundering |
|
100 |
$self->models->set_report_generator_sort_options( |
|
101 |
report => $report, sortable_columns => \@sortable |
|
102 |
); |
|
103 |
} |
|
104 |
my $parts = $self->_get_parts(0); |
|
105 |
my $top = $self->render('disposition_manager/list_parts', { output => 0 }, |
|
106 |
noshow => $noshow, |
|
107 |
PARTS => $parts, |
|
108 |
); |
|
109 |
my $bottom = $noshow ? undef : $self->render( |
|
110 |
'disposition_manager/reorder_level_list/report_bottom', |
|
111 |
{ output => 0}, models => $self->models ); |
|
112 |
$report->set_options( |
|
113 |
raw_top_info_text => $top, |
|
114 |
raw_bottom_info_text => $bottom, |
|
115 |
); |
|
116 |
} |
|
117 |
|
|
118 |
sub action_add_to_purchase_basket{ |
|
119 |
my ($self) = @_; |
|
120 |
|
|
121 |
my $employee = SL::DB::Manager::Employee->current; |
|
122 |
|
|
123 |
my $parts_to_add = delete($::form->{ids}) || []; |
|
124 |
foreach my $id (@{ $parts_to_add }) { |
|
125 |
my $part = SL::DB::Manager::Part->find_by(id => $id) |
|
126 |
or die "Can't find part with id: $id\n"; |
|
127 |
my $needed_qty = $part->order_qty < ($part->rop - $part->onhandqty) ? |
|
128 |
$part->rop - $part->onhandqty |
|
129 |
: $part->order_qty; |
|
130 |
my $basket_part = SL::DB::PurchaseBasketItem->new( |
|
131 |
part_id => $part->id, |
|
132 |
qty => $needed_qty, |
|
133 |
orderer_id => $employee->id, |
|
134 |
)->save; |
|
135 |
} |
|
136 |
|
|
137 |
$self->redirect_to( |
|
138 |
controller => 'DispositionManager', |
|
139 |
action => 'show_basket', |
|
140 |
); |
|
141 |
|
|
142 |
} |
|
143 |
|
|
144 |
sub action_show_basket { |
|
145 |
my ($self) = @_; |
|
146 |
|
|
147 |
$::request->{layout}->add_javascripts( |
|
148 |
'kivi.DispositionManager.js', 'kivi.Part.js' |
|
149 |
); |
|
150 |
my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all( |
|
151 |
query => [ cleared => 'F' ], |
|
152 |
with_objects => [ 'part', 'part.makemodels' ] |
|
153 |
); |
|
154 |
$self->_setup_show_basket_action_bar; |
|
155 |
$self->render( |
|
156 |
'disposition_manager/show_purchase_basket', |
|
157 |
BASKET_ITEMS => $basket_items, |
|
158 |
title => t8('Purchase basket'), |
|
159 |
); |
|
160 |
} |
|
161 |
|
|
162 |
sub action_show_vendor_items { |
|
163 |
my ($self) = @_; |
|
164 |
|
|
165 |
my $makemodels_parts = SL::DB::Manager::Part->get_all( |
|
166 |
query => [ |
|
167 |
'makemodels.make' => $::form->{v_id}, |
|
168 |
'makemodels.sortorder' => 1, |
|
169 |
], |
|
170 |
sort_by => 'onhand', |
|
171 |
with_objects => [ 'makemodels' ] |
|
172 |
); |
|
173 |
$self->render( |
|
174 |
'disposition_manager/_show_vendor_parts', |
|
175 |
{ layout => 0 }, |
|
176 |
MAKEMODEL_ITEMS => $makemodels_parts |
|
177 |
); |
|
178 |
} |
|
179 |
|
|
180 |
sub action_transfer_to_purchase_order { |
|
181 |
my ($self) = @_; |
|
182 |
my @error_report; |
|
183 |
|
|
184 |
my $basket_items_ids = $::form->{ids}; |
|
185 |
my $vendor_items_ids = $::form->{vendor_part_ids}; |
|
186 |
|
|
187 |
unless (($basket_items_ids && scalar @{ $basket_items_ids}) |
|
188 |
|| ( $vendor_items_ids && scalar @{ $vendor_items_ids})) |
|
189 |
{ |
|
190 |
$self->js->flash('error', t8('There are no items selected')); |
|
191 |
return $self->js->render(); |
|
192 |
} |
|
193 |
my $v_id = $::form->{vendor_ids}->[0] ; |
|
194 |
|
|
195 |
my ($vendor, $employee); |
|
196 |
$vendor = SL::DB::Manager::Vendor->find_by(id => $v_id); |
|
197 |
$employee = SL::DB::Manager::Employee->current; |
|
198 |
|
|
199 |
|
|
200 |
my @orderitem_maps = (); # part, qty, orderer_id |
|
201 |
if ($basket_items_ids && scalar @{ $basket_items_ids}) { |
|
202 |
my $basket_items = SL::DB::Manager::PurchaseBasketItem->get_all( |
|
203 |
query => [ id => $basket_items_ids ], |
|
204 |
with_objects => ['part'], |
|
205 |
); |
|
206 |
push @orderitem_maps, map {{ |
|
207 |
part => $_->part, |
|
208 |
qty => $_->qty, |
|
209 |
orderer_id => $_->orderer_id, |
|
210 |
}} @{$basket_items}; |
|
211 |
} |
|
212 |
if ($vendor_items_ids && scalar @{ $vendor_items_ids}) { |
|
213 |
my $vendor_items = SL::DB::Manager::Part->get_all( |
|
214 |
query => [ id => $vendor_items_ids ] ); |
|
215 |
push @orderitem_maps, map {{ |
|
216 |
part => $_, |
|
217 |
qty => $_->order_qty || 1, |
|
218 |
orderer_id => $employee->id, |
|
219 |
}} @{$vendor_items}; |
|
220 |
} |
|
221 |
|
|
222 |
my $order = SL::DB::Order->new( |
|
223 |
vendor_id => $vendor->id, |
|
224 |
employee_id => $employee->id, |
|
225 |
intnotes => $vendor->notes, |
|
226 |
salesman_id => $employee->id, |
|
227 |
payment_id => $vendor->payment_id, |
|
228 |
delivery_term_id => $vendor->delivery_term_id, |
|
229 |
taxzone_id => $vendor->taxzone_id, |
|
230 |
currency_id => $vendor->currency_id, |
|
231 |
transdate => DateTime->today_local |
|
232 |
); |
|
233 |
|
|
234 |
my @order_items; |
|
235 |
my $i = 0; |
|
236 |
foreach my $orderitem_map (@orderitem_maps) { |
|
237 |
$i++; |
|
238 |
my $part = $orderitem_map->{part}; |
|
239 |
my $qty = $orderitem_map->{qty}; |
|
240 |
my $orderer_id = $orderitem_map->{orderer_id}; |
|
241 |
|
|
242 |
my $order_item = SL::DB::OrderItem->new( |
|
243 |
part => $part, |
|
244 |
qty => $qty, |
|
245 |
unit => $part->unit, |
|
246 |
description => $part->description, |
|
247 |
price_factor_id => $part->price_factor_id, |
|
248 |
price_factor => |
|
249 |
$part->price_factor_id ? $part->price_factor->factor |
|
250 |
: '', |
|
251 |
orderer_id => $orderer_id, |
|
252 |
position => $i, |
|
253 |
); |
|
254 |
|
|
255 |
my $price_source = SL::PriceSource->new( |
|
256 |
record_item => $order_item, record => $order); |
|
257 |
$order_item->sellprice( |
|
258 |
$price_source->best_price ? $price_source->best_price->price |
|
259 |
: 0); |
|
260 |
$order_item->active_price_source( |
|
261 |
$price_source->best_price ? $price_source->best_price->source |
|
262 |
: ''); |
|
263 |
push @order_items, $order_item; |
|
264 |
} |
|
265 |
|
|
266 |
$order->assign_attributes(orderitems => \@order_items); |
|
267 |
|
|
268 |
$order->db->with_transaction( sub { |
|
269 |
$order->calculate_prices_and_taxes; |
|
270 |
$order->save; |
|
271 |
|
|
272 |
my $snumbers = "ordernumber_" . $order->ordnumber; |
|
273 |
SL::DB::History->new( |
|
274 |
trans_id => $order->id, |
|
275 |
snumbers => $snumbers, |
|
276 |
employee_id => SL::DB::Manager::Employee->current->id, |
|
277 |
addition => 'SAVED', |
|
278 |
what_done => 'PurchaseBasket->Order', |
|
279 |
)->save(); |
|
280 |
foreach my $item(@{ $order->orderitems }){ |
|
281 |
$item->parse_custom_variable_values->save; |
|
282 |
$item->{custom_variables} = \@{ $item->cvars_by_config }; |
|
283 |
$item->save; |
|
284 |
} |
|
285 |
if ($basket_items_ids && scalar @{ $basket_items_ids}) { |
|
286 |
SL::DB::Manager::PurchaseBasketItem->delete_all( |
|
287 |
where => [ id => $basket_items_ids]); |
|
288 |
} |
|
289 |
return 1; |
|
290 |
}) || die "error: " . $order->db->error; |
|
291 |
|
|
292 |
$self->redirect_to( |
|
293 |
controller => 'Order', |
|
294 |
action => 'edit', |
|
295 |
type => 'purchase_order', |
|
296 |
vc => 'vendor', |
|
297 |
id => $order->id, |
|
298 |
); |
|
299 |
} |
|
300 |
|
|
301 |
sub action_delete_purchase_basket_items { |
|
302 |
|
|
303 |
my ($self) = @_; |
|
304 |
my @error_report; |
|
305 |
|
|
306 |
my $basket_item_ids = $::form->{ids}; |
|
307 |
|
|
308 |
$main::lxdebug->dump(0, "TST: basket_items_ids", $basket_item_ids); |
|
309 |
|
|
310 |
if ($basket_item_ids && scalar @{ $basket_item_ids}) { |
|
311 |
SL::DB::Manager::PurchaseBasketItem->delete_all( |
|
312 |
where => [ id => $basket_item_ids]); |
|
313 |
} else { |
|
314 |
$self->js->flash('error', t8('There are no items selected')); |
|
315 |
return $self->js->render(); |
|
316 |
} |
|
317 |
|
|
318 |
flash_later('info', t8('Selected items deleted')); |
|
319 |
|
|
320 |
$self->redirect_to( |
|
321 |
controller => 'DispositionManager', |
|
322 |
action => 'show_basket', |
|
323 |
); |
|
324 |
} |
|
325 |
|
|
326 |
sub _get_parts { |
|
327 |
my ($self, $ordered) = @_; |
|
328 |
|
|
329 |
my $query = <<SQL; |
|
330 |
WITH available AS ( |
|
331 |
SELECT inv.parts_id, sum(qty) as sum |
|
332 |
FROM inventory inv |
|
333 |
LEFT JOIN warehouse w ON inv.warehouse_id = w.id |
|
334 |
WHERE NOT w.invalid |
|
335 |
GROUP BY inv.parts_id |
|
336 |
|
|
337 |
UNION ALL |
|
338 |
|
|
339 |
SELECT p.id, 0 as sum |
|
340 |
FROM parts p |
|
341 |
WHERE p.id NOT IN ( SELECT distinct parts_id from inventory) |
|
342 |
AND NOT p.obsolete |
|
343 |
AND p.rop != 0 |
|
344 |
) |
|
345 |
|
|
346 |
SELECT p.id |
|
347 |
FROM parts p |
|
348 |
LEFT JOIN available ava ON ava.parts_id = p.id |
|
349 |
WHERE ( ava.sum < p.rop ) |
|
350 |
AND p.id NOT IN ( SELECT part_id FROM purchase_basket_items ) |
|
351 |
AND NOT p.obsolete |
|
352 |
ORDER BY p.partnumber |
|
353 |
SQL |
|
354 |
my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query); |
|
355 |
return unless scalar @ids; |
|
356 |
my $parts = SL::DB::Manager::Part->get_all( query => [ id => \@ids ] ); |
|
357 |
my $parts_to_order = [ grep { !$_->get_open_ordered_qty } @{$parts} ]; |
|
358 |
return $parts_to_order if !$ordered; |
|
359 |
my $parts_ordered = [ |
|
360 |
map { $_->id } grep { $_->get_open_ordered_qty } @{$parts} |
|
361 |
]; |
|
362 |
return $parts_ordered if $ordered; |
|
363 |
}; |
|
364 |
|
|
365 |
sub init_models { |
|
366 |
my ($self) = @_; |
|
367 |
my $parts1 = $self->_get_parts(1) || []; |
|
368 |
my @parts = @{$parts1}; |
|
369 |
my $get_models = SL::Controller::Helper::GetModels->new( |
|
370 |
controller => $self, |
|
371 |
model => 'Part', |
|
372 |
sorted => { |
|
373 |
_default => { |
|
374 |
by => 'partnumber', |
|
375 |
dir => 1, |
|
376 |
}, |
|
377 |
partnumber => $::locale->text('Part Number'), |
|
378 |
description => $::locale->text('Description'), |
|
379 |
}, |
|
380 |
query => [ |
|
381 |
(id => \@parts) x !!@parts, |
|
382 |
], |
|
383 |
paginated => { |
|
384 |
form_params => [ qw(page per_page) ], |
|
385 |
per_page => 35, |
|
386 |
} |
|
387 |
); |
|
388 |
return $get_models; |
|
389 |
} |
|
390 |
|
|
391 |
|
|
392 |
|
|
393 |
sub _setup_list_action_bar { |
|
394 |
my ($self) = @_; |
|
395 |
for my $bar ($::request->layout->get('actionbar')) { |
|
396 |
$bar->add( |
|
397 |
action => [ |
|
398 |
t8('Action'), |
|
399 |
submit => [ |
|
400 |
'#form', { action => "DispositionManager/add_to_purchase_basket" } ], |
|
401 |
tooltip => t8('Add to purchase basket'), |
|
402 |
], |
|
403 |
); |
|
404 |
} |
|
405 |
} |
|
406 |
|
|
407 |
sub _setup_show_basket_action_bar { |
|
408 |
my ($self) = @_; |
|
409 |
for my $bar ($::request->layout->get('actionbar')) { |
|
410 |
$bar->add( |
|
411 |
action => [ |
|
412 |
t8('Reload'), |
|
413 |
link => $self->url_for( |
|
414 |
controller => 'DispositionManager', |
|
415 |
action => 'show_basket', |
|
416 |
), |
|
417 |
], |
|
418 |
action => [ |
|
419 |
t8('Action'), |
|
420 |
call => [ 'kivi.DispositionManager.create_order' ], |
|
421 |
tooltip => t8('Create purchase order'), |
|
422 |
], |
|
423 |
action => [ |
|
424 |
t8('Delete'), |
|
425 |
call => [ 'kivi.DispositionManager.delete_purchase_basket_items' ], |
|
426 |
tooltip => t8('Delete selected from purchase basket'), |
|
427 |
], |
|
428 |
); |
|
429 |
} |
|
430 |
} |
|
431 |
1; |
|
432 |
|
|
433 |
__END__ |
|
434 |
|
|
435 |
=encoding utf-8 |
|
436 |
|
|
437 |
=head1 NAME |
|
438 |
|
|
439 |
SL::Controller::DispositionManager Controller to manage purchase orders for parts |
|
440 |
|
|
441 |
=head1 DESCRIPTION |
|
442 |
|
|
443 |
This controller shows a list of parts using the filter minimum stock (rop). |
|
444 |
From this list it is possible to put parts in a purchase basket to order. |
|
445 |
It's also possible to put parts from the parts edit form in the purchase basket. |
|
446 |
|
|
447 |
From the purchase basket you can create a purchase order by using the filter vendor. |
|
448 |
The quantity to order will be prefilled by the value min_qty_to_order from parts or |
|
449 |
makemodel(vendor_parts) or default to qty 1. |
|
450 |
|
|
451 |
Tables: |
|
452 |
|
|
453 |
=over 2 |
|
454 |
|
|
455 |
=item purchase_basket |
|
456 |
|
|
457 |
=back |
|
458 |
|
|
459 |
Dependencies: |
|
460 |
|
|
461 |
=over 2 |
|
462 |
|
|
463 |
=item parts |
|
464 |
|
|
465 |
=item makemodels |
|
466 |
|
|
467 |
|
|
468 |
=back |
|
469 |
|
|
470 |
=head1 URL ACTIONS |
|
471 |
|
|
472 |
=over 4 |
|
473 |
|
|
474 |
=item C<action_list_parts> |
|
475 |
|
|
476 |
List the parts by the filter min stock (rop) and not in an open purchase order. |
|
477 |
|
|
478 |
=item C<action_add_to_purchase_basket> |
|
479 |
|
|
480 |
Adds one or more parts to the purchase basket. |
|
481 |
|
|
482 |
=item C<action_show_basket> |
|
483 |
|
|
484 |
Shows a list with parts which are in the basket. |
|
485 |
This list can be filtered by vendor. Then you can create a purchase order. |
|
486 |
When filtered by vendor, a table with the parts from the vendor of the purchase basket and |
|
487 |
a table with all parts from the vendor will be shown. From there you can mark |
|
488 |
the parts and create an order |
|
489 |
|
|
490 |
=item C<action_transfer_to_purchase_order> |
|
491 |
|
|
492 |
Transfers the marked and by vendor filtered parts to a purchase order. |
|
493 |
Deletes the entry in the purchase basket. |
|
494 |
|
|
495 |
=back |
|
496 |
|
|
497 |
=head1 BUGS |
|
498 |
|
|
499 |
None yet. :) |
|
500 |
|
|
501 |
=head1 AUTHOR |
|
502 |
|
|
503 |
W. Hahn E<lt>wh@futureworldsearch.netE<gt> |
|
504 |
|
|
505 |
=cut |
Auch abrufbar als: Unified diff
DispositionManager: Controller hinzugefügt