Kosmetik: Einrückung gefixt
[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   $self->{orderitems} = $self->get_models(%{ $self->db_args });
55
56   $self->list_objects;
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',    { no_output => 1, partial => 1 }),
181     raw_bottom_info_text  => $self->render('delivery_plan/report_bottom', { no_output => 1, partial => 1 }),
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   $self->{report_data} = {
195     column_defs        => \%column_defs,
196     columns            => \@columns,
197   };
198 }
199
200 sub list_objects {
201   my ($self) = @_;
202   my $column_defs = $self->{report_data}{column_defs};
203   for my $obj (@{ $self->{orderitems} || [] }) {
204     $self->{report}->add_data({
205       map {
206         $_ => {
207           data => $column_defs->{$_}{sub} ? $column_defs->{$_}{sub}->($obj)
208                 : $obj->can($_)           ? $obj->$_
209                 :                           $obj->{$_},
210           link => $column_defs->{$_}{obj_link} ? $column_defs->{$_}{obj_link}->($obj) : '',
211         },
212       } @{ $self->{report_data}{columns} || {} }
213     });
214   }
215
216   return $self->{report}->generate_with_headers;
217 }
218
219 sub make_filter_summary {
220   my ($self) = @_;
221
222   my $filter = $::form->{filter} || {};
223   my @filter_strings;
224
225   my @filters = (
226     [ $filter->{order}{"ordnumber:substr::ilike"},                $::locale->text('Number')                                             ],
227     [ $filter->{part}{"partnumber:substr::ilike"},                $::locale->text('Part Number')                                        ],
228     [ $filter->{"description:substr::ilike"},                     $::locale->text('Part Description')                                   ],
229     [ $filter->{"reqdate:date::ge"},                              $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
230     [ $filter->{"reqdate:date::le"},                              $::locale->text('Delivery Date') . " " . $::locale->text('To Date')   ],
231     [ $filter->{"qty:number"},                                    $::locale->text('Quantity')                                           ],
232     [ $filter->{order}{customer}{"name:substr::ilike"},           $::locale->text('Customer')                                           ],
233     [ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number')                                    ],
234   );
235
236   my @flags = (
237     [ $filter->{part}{type}{part},     $::locale->text('Parts')      ],
238     [ $filter->{part}{type}{service},  $::locale->text('Services')   ],
239     [ $filter->{part}{type}{assembly}, $::locale->text('Assemblies') ],
240   );
241
242   for (@flags) {
243     push @filter_strings, "$_->[1]" if $_->[0];
244   }
245   for (@filters) {
246     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
247   }
248
249   $self->{filter_summary} = join ', ', @filter_strings;
250 }
251
252 sub link_to {
253   my ($self, $object, %params) = @_;
254
255   return unless $object;
256   my $action = $params{action} || 'edit';
257
258   if ($object->isa('SL::DB::Order')) {
259     my $type   = $object->type;
260     my $vc     = $object->is_sales ? 'customer' : 'vendor';
261     my $id     = $object->id;
262
263     return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
264   }
265   if ($object->isa('SL::DB::Part')) {
266     my $id     = $object->id;
267     return "ic.pl?action=$action&id=$id";
268   }
269   if ($object->isa('SL::DB::Customer')) {
270     my $id     = $object->id;
271     return "ct.pl?action=$action&id=$id&db=customer";
272   }
273 }
274
275 # unfortunately ParseFilter can't handle compount filters.
276 # so we clone the original filter (still need that for serializing)
277 # rip out the options we know an replace them with the compound options.
278 # ParseFilter will take care of the prefixing then.
279 sub _pre_parse_filter {
280   my ($self, $orig_filter, $launder_to) = @_;
281
282   return undef unless $orig_filter;
283
284   my $filter = clone($orig_filter);
285   if ($filter->{part} && $filter->{part}{type}) {
286     $launder_to->{part}{type} = delete $filter->{part}{type};
287     my @part_filters = grep $_, map {
288       $launder_to->{part}{type}{$_} ? SL::DB::Manager::Part->type_filter($_) : ()
289     } qw(part service assembly);
290
291     push @{ $filter->{and} }, or => [ @part_filters ] if @part_filters;
292   }
293
294   for my $op (qw(le ge)) {
295     if ($filter->{"reqdate:date::$op"}) {
296       $launder_to->{"reqdate_date__$op"} = delete $filter->{"reqdate:date::$op"};
297       my $parsed_date = DateTime->from_lxoffice($launder_to->{"reqdate_date__$op"});
298       push @{ $filter->{and} }, or => [
299         'reqdate' => { $op => $parsed_date },
300         and => [
301           'reqdate' => undef,
302           'order.reqdate' => { $op => $parsed_date },
303         ]
304       ] if $parsed_date;
305     }
306   }
307
308   if (my $style = delete $filter->{searchstyle}) {
309     $self->{searchstyle}       = $style;
310     $launder_to->{searchstyle} = $style;
311   }
312
313   return $filter;
314 }
315
316 1;