Merge branch 'master' of vc.linet-services.de:public/lx-office-erp
[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     )
124   ];
125
126   return \%args;
127 }
128
129 sub prepare_report {
130   my ($self)      = @_;
131
132   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
133   $self->{report} = $report;
134
135   my @columns     = qw(reqdate customer ordnumber partnumber description qty shipped_qty not_shipped_qty);
136   my @sortable    = qw(reqdate customer ordnumber partnumber description                );
137
138   my %column_defs = (
139     reqdate           => {      sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date                         } },
140     description       => {      sub => sub { $_[0]->description                                                              },
141                             obj_link => sub { $self->link_to($_[0]->part)                                                     } },
142     partnumber        => {      sub => sub { $_[0]->part->partnumber                                                         },
143                             obj_link => sub { $self->link_to($_[0]->part)                                                     } },
144     qty               => {      sub => sub { $_[0]->qty_as_number . ' ' . $_[0]->unit                                        } },
145     shipped_qty       => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit } },
146     not_shipped_qty   => {      sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]->shipped_qty, 2) . ' ' . $_[0]->unit } },
147     ordnumber         => {      sub => sub { $_[0]->order->ordnumber                                                         },
148                             obj_link => sub { $self->link_to($_[0]->order)                                                    } },
149     customer          => {      sub => sub { $_[0]->order->customer->name                                                    },
150                             obj_link => sub { $self->link_to($_[0]->order->customer)                                          } },
151   );
152
153   map { $column_defs{$_}->{text} = $::locale->text( $self->get_sort_spec->{$_}->{title} ) } keys %column_defs;
154
155   $report->set_options(
156     std_column_visibility => 1,
157     controller_class      => 'DeliveryPlan',
158     output_format         => 'HTML',
159     top_info_text         => $::locale->text('Delivery Plan for currently outstanding sales orders'),
160     raw_top_info_text     => $self->render('delivery_plan/report_top',    { no_output => 1, partial => 1 }),
161     raw_bottom_info_text  => $self->render('delivery_plan/report_bottom', { no_output => 1, partial => 1 }),
162     title                 => $::locale->text('Delivery Plan'),
163     allow_pdf_export      => 1,
164     allow_csv_export      => 1,
165   );
166   $report->set_columns(%column_defs);
167   $report->set_column_order(@columns);
168   $report->set_export_options(qw(list filter));
169   $report->set_options_from_form;
170   $self->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
171
172   $self->disable_pagination if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
173
174   $self->{report_data} = {
175     column_defs        => \%column_defs,
176     columns            => \@columns,
177   };
178 }
179
180 sub list_objects {
181   my ($self) = @_;
182   my $column_defs = $self->{report_data}{column_defs};
183   for my $obj (@{ $self->{orderitems} || [] }) {
184     $self->{report}->add_data({
185       map {
186         $_ => {
187           data => $column_defs->{$_}{sub} ? $column_defs->{$_}{sub}->($obj)
188                 : $obj->can($_)           ? $obj->$_
189                 :                           $obj->{$_},
190           link => $column_defs->{$_}{obj_link} ? $column_defs->{$_}{obj_link}->($obj) : '',
191         },
192       } @{ $self->{report_data}{columns} || {} }
193     });
194   }
195
196   return $self->{report}->generate_with_headers;
197 }
198
199 sub make_filter_summary {
200   my ($self) = @_;
201
202   my $filter = $::form->{filter} || {};
203   my @filter_strings;
204
205   my @filters = (
206     [ $filter->{order}{"ordnumber:substr::ilike"},                $::locale->text('Number')                                             ],
207     [ $filter->{part}{"partnumber:substr::ilike"},                $::locale->text('Part Number')                                        ],
208     [ $filter->{"description:substr::ilike"},                     $::locale->text('Part Description')                                   ],
209     [ $filter->{"reqdate:date::ge"},                              $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
210     [ $filter->{"reqdate:date::le"},                              $::locale->text('Delivery Date') . " " . $::locale->text('To Date')   ],
211     [ $filter->{"qty:number"},                                    $::locale->text('Quantity')                                           ],
212     [ $filter->{order}{customer}{"name:substr::ilike"},           $::locale->text('Customer')                                           ],
213     [ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number')                                    ],
214   );
215
216   my @flags = (
217     [ $filter->{part}{type}{part},     $::locale->text('Parts')      ],
218     [ $filter->{part}{type}{service},  $::locale->text('Services')   ],
219     [ $filter->{part}{type}{assembly}, $::locale->text('Assemblies') ],
220   );
221
222   for (@flags) {
223     push @filter_strings, "$_->[1]" if $_->[0];
224   }
225   for (@filters) {
226     push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
227   }
228
229   $self->{filter_summary} = join ', ', @filter_strings;
230 }
231
232 sub link_to {
233   my ($self, $object, %params) = @_;
234
235   return unless $object;
236   my $action = $params{action} || 'edit';
237
238   if ($object->isa('SL::DB::Order')) {
239     my $type   = $object->type;
240     my $vc     = $object->is_sales ? 'customer' : 'vendor';
241     my $id     = $object->id;
242
243     return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
244   }
245   if ($object->isa('SL::DB::Part')) {
246     my $id     = $object->id;
247     return "ic.pl?action=$action&id=$id";
248   }
249   if ($object->isa('SL::DB::Customer')) {
250     my $id     = $object->id;
251     return "ct.pl?action=$action&id=$id&db=customer";
252   }
253 }
254
255 # unfortunately ParseFilter can't handle compount filters.
256 # so we clone the original filter (still need that for serializing)
257 # rip out the options we know an replace them with the compound options.
258 # ParseFilter will take care of the prefixing then.
259 sub _pre_parse_filter {
260   my ($self, $orig_filter, $launder_to) = @_;
261
262   return undef unless $orig_filter;
263
264   my $filter = clone($orig_filter);
265   if ($filter->{part} && $filter->{part}{type}) {
266     $launder_to->{part}{type} = delete $filter->{part}{type};
267     my @part_filters = grep $_, map {
268       $launder_to->{part}{type}{$_} ? SL::DB::Manager::Part->type_filter($_) : ()
269     } qw(part service assembly);
270
271     push @{ $filter->{and} }, or => [ @part_filters ] if @part_filters;
272   }
273
274   for my $op (qw(le ge)) {
275     if ($filter->{"reqdate:date::$op"}) {
276       $launder_to->{"reqdate_date__$op"} = delete $filter->{"reqdate:date::$op"};
277       my $parsed_date = DateTime->from_lxoffice($launder_to->{"reqdate_date__$op"});
278       push @{ $filter->{and} }, or => [
279         'reqdate' => { $op => $parsed_date },
280         and => [
281           'reqdate' => undef,
282           'order.reqdate' => { $op => $parsed_date },
283         ]
284       ] if $parsed_date;
285     }
286   }
287
288   if (my $style = delete $filter->{searchstyle}) {
289     $self->{searchstyle}       = $style;
290     $launder_to->{searchstyle} = $style;
291   }
292
293   return $filter;
294 }
295
296 1;