Refactoring: list_objects() aus Controllern in ReportGenerator-Helfer verschieben
[kivitendo-erp.git] / SL / Controller / DeliveryPlan.pm
1 package SL::Controller::DeliveryPlan;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use Clone qw(clone);
7 use SL::DB::OrderItem;
8 use SL::Controller::Helper::GetModels;
9 use SL::Controller::Helper::Paginated;
10 use SL::Controller::Helper::Sorted;
11 use SL::Controller::Helper::ParseFilter;
12 use SL::Controller::Helper::ReportGenerator;
13 use SL::Locale::String;
14
15 use Rose::Object::MakeMethods::Generic (
16   scalar => [ qw(db_args flat_filter) ],
17 );
18
19 __PACKAGE__->run_before(sub { $::auth->assert('sales_order_edit'); });
20
21 __PACKAGE__->get_models_url_params('flat_filter');
22 __PACKAGE__->make_paginated(
23   MODEL         => 'OrderItem',
24   PAGINATE_ARGS => 'db_args',
25   ONLY          => [ qw(list) ],
26 );
27
28 __PACKAGE__->make_sorted(
29   MODEL             => 'OrderItem',
30   ONLY              => [ qw(list) ],
31
32   DEFAULT_BY        => 'reqdate',
33   DEFAULT_DIR       => 1,
34
35   reqdate           => t8('Reqdate'),
36   description       => t8('Description'),
37   partnumber        => t8('Part Number'),
38   qty               => t8('Qty'),
39   shipped_qty       => t8('shipped'),
40   not_shipped_qty   => t8('not shipped'),
41   ordnumber         => t8('Order'),
42   customer          => t8('Customer'),
43 );
44
45 sub action_list {
46   my ($self) = @_;
47
48   $self->db_args($self->setup_for_list(filter => $::form->{filter}));
49   $self->flat_filter({ map { $_->{key} => $_->{value} } $::form->flatten_variables('filter') });
50   $self->make_filter_summary;
51
52   $self->prepare_report;
53
54   my $orderitems = $self->get_models(%{ $self->db_args });
55
56   $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
57 }
58
59 # private functions
60
61 sub setup_for_list {
62   my ($self, %params) = @_;
63   $self->{filter} = {};
64   my %args = (
65     parse_filter(
66       $self->_pre_parse_filter($::form->{filter}, $self->{filter}),
67       with_objects => [ 'order', 'order.customer', 'part' ],
68       launder_to => $self->{filter},
69     ),
70   );
71
72   $args{query} = [ @{ $args{query} || [] },
73     (
74       'order.customer_id' => { gt => 0 },
75       'order.closed' => 0,
76       or => [ 'order.quotation' => 0, 'order.quotation' => undef ],
77
78       # filter by shipped_qty < qty, read from innermost to outermost
79       'id' => [ \"
80         -- 3. resolve the desired information about those
81         SELECT oi.id FROM (
82           -- 2. slice only part, orderitem and both quantities from it
83           SELECT parts_id, trans_id, qty, SUM(doi_qty) AS doi_qty FROM (
84             -- 1. join orderitems and deliverorder items via record_links.
85             --    also add customer data to filter for sales_orders
86             SELECT oi.parts_id, oi.trans_id, oi.id, oi.qty, doi.qty AS doi_qty
87             FROM orderitems oi, oe, record_links rl, delivery_order_items doi
88             WHERE
89               oe.id = oi.trans_id AND
90               oe.customer_id IS NOT NULL AND
91               (oe.quotation = 'f' OR oe.quotation IS NULL) AND
92               NOT oe.closed AND
93               rl.from_id = oe.id AND
94               rl.from_id = oi.trans_id AND
95               oe.id = oi.trans_id AND
96               rl.from_table = 'oe' AND
97               rl.to_table = 'delivery_orders' AND
98               rl.to_id = doi.delivery_order_id AND
99               oi.parts_id = doi.parts_id
100           ) tuples GROUP BY parts_id, trans_id, qty
101         ) partials
102         LEFT JOIN orderitems oi ON partials.parts_id = oi.parts_id AND partials.trans_id = oi.trans_id
103         WHERE oi.qty > doi_qty
104
105         UNION ALL
106
107         -- 4. since the join over record_links fails for sales_orders wihtout any delivery order
108         --    retrieve those without record_links at all
109         SELECT oi.id FROM orderitems oi, oe
110         WHERE
111           oe.id = oi.trans_id AND
112           oe.customer_id IS NOT NULL AND
113           (oe.quotation = 'f' OR oe.quotation IS NULL) AND
114           NOT oe.closed AND
115           oi.trans_id NOT IN (
116             SELECT from_id
117             FROM record_links rl
118             WHERE
119               rl.from_table ='oe' AND
120               rl.to_table = 'delivery_orders'
121           )
122
123         UNION ALL
124
125         -- 5. In case someone deleted a line of the delivery_order there will be a record_link (4 fails)
126         --    but there won't be a delivery_order_items to find (3 fails too). Search for orphaned orderitems this way
127         SELECT oi.id FROM orderitems AS oi, oe, record_links AS rl
128         WHERE
129           rl.from_table = 'oe' AND
130           rl.to_table = 'delivery_orders' AND
131
132           oi.trans_id = rl.from_id AND
133           oi.parts_id NOT IN (
134             SELECT doi.parts_id FROM delivery_order_items AS doi WHERE doi.delivery_order_id = rl.to_id
135           ) AND
136
137           oe.id = oi.trans_id AND
138
139           oe.customer_id IS NOT NULL AND
140           (oe.quotation = 'f' OR oe.quotation IS NULL) AND
141           NOT oe.closed
142       " ],
143     )
144   ];
145
146   return \%args;
147 }
148
149 sub prepare_report {
150   my ($self)      = @_;
151
152   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
153   $self->{report} = $report;
154
155   my @columns     = qw(reqdate customer ordnumber partnumber description qty shipped_qty not_shipped_qty);
156   my @sortable    = qw(reqdate customer ordnumber partnumber description);
157
158   my %column_defs = (
159     reqdate           => {      sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date                         } },
160     description       => {      sub => sub { $_[0]->description                                                              },
161                            obj_link => sub { $self->link_to($_[0]->part)                                                     } },
162     partnumber        => {      sub => sub { $_[0]->part->partnumber                                                         },
163                            obj_link => sub { $self->link_to($_[0]->part)                                                     } },
164     qty               => {      sub => sub { $_[0]->qty_as_number . ' ' . $_[0]->unit                                        } },
165     shipped_qty       => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit } },
166     not_shipped_qty   => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit } },
167     ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                                         },
168                            obj_link => sub { $self->link_to($_[0]->order)                                                    } },
169     customer          => {      sub => sub { $_[0]->order->customer->name                                                    },
170                            obj_link => sub { $self->link_to($_[0]->order->customer)                                          } },
171   );
172
173   map { $column_defs{$_}->{text} = $::locale->text( $self->get_sort_spec->{$_}->{title} ) } keys %column_defs;
174
175   $report->set_options(
176     std_column_visibility => 1,
177     controller_class      => 'DeliveryPlan',
178     output_format         => 'HTML',
179     top_info_text         => $::locale->text('Delivery Plan for currently outstanding sales orders'),
180     raw_top_info_text     => $self->render('delivery_plan/report_top',    { output => 0 }),
181     raw_bottom_info_text  => $self->render('delivery_plan/report_bottom', { output => 0 }),
182     title                 => $::locale->text('Delivery Plan'),
183     allow_pdf_export      => 1,
184     allow_csv_export      => 1,
185   );
186   $report->set_columns(%column_defs);
187   $report->set_column_order(@columns);
188   $report->set_export_options(qw(list filter));
189   $report->set_options_from_form;
190   $self->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
191
192   $self->disable_pagination if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
193 }
194
195 sub make_filter_summary {
196   my ($self) = @_;
197
198   my $filter = $::form->{filter} || {};
199   my @filter_strings;
200
201   my @filters = (
202     [ $filter->{order}{"ordnumber:substr::ilike"},                $::locale->text('Number')                                             ],
203     [ $filter->{part}{"partnumber:substr::ilike"},                $::locale->text('Part Number')                                        ],
204     [ $filter->{"description:substr::ilike"},                     $::locale->text('Part Description')                                   ],
205     [ $filter->{"reqdate:date::ge"},                              $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
206     [ $filter->{"reqdate:date::le"},                              $::locale->text('Delivery Date') . " " . $::locale->text('To Date')   ],
207     [ $filter->{"qty:number"},                                    $::locale->text('Quantity')                                           ],
208     [ $filter->{order}{customer}{"name:substr::ilike"},           $::locale->text('Customer')                                           ],
209     [ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number')                                    ],
210   );
211
212   my @flags = (
213     [ $filter->{part}{type}{part},     $::locale->text('Parts')      ],
214     [ $filter->{part}{type}{service},  $::locale->text('Services')   ],
215     [ $filter->{part}{type}{assembly}, $::locale->text('Assemblies') ],
216   );
217
218   for (@flags) {
219     push @filter_strings, "$_->[1]" if $_->[0];
220   }
221   for (@filters) {
222     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
223   }
224
225   $self->{filter_summary} = join ', ', @filter_strings;
226 }
227
228 sub link_to {
229   my ($self, $object, %params) = @_;
230
231   return unless $object;
232   my $action = $params{action} || 'edit';
233
234   if ($object->isa('SL::DB::Order')) {
235     my $type   = $object->type;
236     my $vc     = $object->is_sales ? 'customer' : 'vendor';
237     my $id     = $object->id;
238
239     return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
240   }
241   if ($object->isa('SL::DB::Part')) {
242     my $id     = $object->id;
243     return "ic.pl?action=$action&id=$id";
244   }
245   if ($object->isa('SL::DB::Customer')) {
246     my $id     = $object->id;
247     return "ct.pl?action=$action&id=$id&db=customer";
248   }
249 }
250
251 # unfortunately ParseFilter can't handle compount filters.
252 # so we clone the original filter (still need that for serializing)
253 # rip out the options we know an replace them with the compound options.
254 # ParseFilter will take care of the prefixing then.
255 sub _pre_parse_filter {
256   my ($self, $orig_filter, $launder_to) = @_;
257
258   return undef unless $orig_filter;
259
260   my $filter = clone($orig_filter);
261   if ($filter->{part} && $filter->{part}{type}) {
262     $launder_to->{part}{type} = delete $filter->{part}{type};
263     my @part_filters = grep $_, map {
264       $launder_to->{part}{type}{$_} ? SL::DB::Manager::Part->type_filter($_) : ()
265     } qw(part service assembly);
266
267     push @{ $filter->{and} }, or => [ @part_filters ] if @part_filters;
268   }
269
270   for my $op (qw(le ge)) {
271     if ($filter->{"reqdate:date::$op"}) {
272       $launder_to->{"reqdate_date__$op"} = delete $filter->{"reqdate:date::$op"};
273       my $parsed_date = DateTime->from_lxoffice($launder_to->{"reqdate_date__$op"});
274       push @{ $filter->{and} }, or => [
275         'reqdate' => { $op => $parsed_date },
276         and => [
277           'reqdate' => undef,
278           'order.reqdate' => { $op => $parsed_date },
279         ]
280       ] if $parsed_date;
281     }
282   }
283
284   if (my $style = delete $filter->{searchstyle}) {
285     $self->{searchstyle}       = $style;
286     $launder_to->{searchstyle} = $style;
287   }
288
289   return $filter;
290 }
291
292 1;