7f12aae2555166f3236ed8fb3830e00bb8fe2657
[kivitendo-erp.git] / SL / DB / DeliveryOrder.pm
1 package SL::DB::DeliveryOrder;
2
3 use strict;
4
5 use Carp;
6
7 use Rose::DB::Object::Helpers ();
8
9 use SL::DB::MetaSetup::DeliveryOrder;
10 use SL::DB::Manager::DeliveryOrder;
11 use SL::DB::Helper::AttrHTML;
12 use SL::DB::Helper::AttrSorted;
13 use SL::DB::Helper::FlattenToForm;
14 use SL::DB::Helper::LinkedRecords;
15 use SL::DB::Helper::TransNumberGenerator;
16
17 use List::Util qw(first);
18
19 __PACKAGE__->meta->add_relationship(orderitems => { type         => 'one to many',
20                                                     class        => 'SL::DB::DeliveryOrderItem',
21                                                     column_map   => { id => 'delivery_order_id' },
22                                                     manager_args => { with_objects => [ 'part' ] }
23                                                   },
24                                     custom_shipto => {
25                                       type        => 'one to one',
26                                       class       => 'SL::DB::Shipto',
27                                       column_map  => { id => 'trans_id' },
28                                       query_args  => [ module => 'DO' ],
29                                     },
30                                    );
31
32 __PACKAGE__->meta->initialize;
33
34 __PACKAGE__->attr_html('notes');
35 __PACKAGE__->attr_sorted('items');
36
37 __PACKAGE__->before_save('_before_save_set_donumber');
38
39 # hooks
40
41 sub _before_save_set_donumber {
42   my ($self) = @_;
43
44   $self->create_trans_number if !$self->donumber;
45
46   return 1;
47 }
48
49 # methods
50
51 sub items { goto &orderitems; }
52 sub add_items { goto &add_orderitems; }
53 sub payment_terms { goto &payment; }
54
55 sub sales_order {
56   my $self   = shift;
57   my %params = @_;
58
59
60   require SL::DB::Order;
61   my $orders = SL::DB::Manager::Order->get_all(
62     query => [
63       ordnumber => $self->ordnumber,
64       @{ $params{query} || [] },
65     ],
66   );
67
68   return first { $_->is_type('sales_order') } @{ $orders };
69 }
70
71 sub type {
72   return shift->customer_id ? 'sales_delivery_order' : 'purchase_delivery_order';
73 }
74
75 sub displayable_type {
76   my $type = shift->type;
77
78   return $::locale->text('Sales Delivery Order')    if $type eq 'sales_delivery_order';
79   return $::locale->text('Purchase Delivery Order') if $type eq 'purchase_delivery_order';
80
81   die 'invalid type';
82 }
83
84
85 sub displayable_state {
86   my ($self) = @_;
87
88   return join '; ',
89     ($self->closed    ? $::locale->text('closed')    : $::locale->text('open')),
90     ($self->delivered ? $::locale->text('delivered') : $::locale->text('not delivered'));
91 }
92
93 sub date {
94   goto &transdate;
95 }
96
97 sub _clone_orderitem_cvar {
98   my ($cvar) = @_;
99
100   my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
101   $cloned->sub_module('delivery_order_items');
102
103   return $cloned;
104 }
105
106 sub new_from {
107   my ($class, $source, %params) = @_;
108
109   croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) eq 'SL::DB::Order';
110
111   my ($item_parent_id_column, $item_parent_column);
112
113   if (ref($source) eq 'SL::DB::Order') {
114     $item_parent_id_column = 'trans_id';
115     $item_parent_column    = 'order';
116   }
117
118   my %args = ( map({ ( $_ => $source->$_ ) } qw(cp_id currency_id customer_id cusordnumber department_id employee_id globalproject_id intnotes language_id notes
119                                                 ordnumber payment_id reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id transaction_description vendor_id
120                                              )),
121                closed    => 0,
122                is_sales  => !!$source->customer_id,
123                delivered => 0,
124                transdate => DateTime->today_local,
125             );
126
127   # Custom shipto addresses (the ones specific to the sales/purchase
128   # record and not to the customer/vendor) are only linked from
129   # shipto -> delivery_orders. Meaning delivery_orders.shipto_id
130   # will not be filled in that case. Therefore we have to return the
131   # new shipto object as a separate object so that the caller can
132   # save it, too.
133   my $custom_shipto;
134   if (!$source->shipto_id && $source->id) {
135     my $old = $source->custom_shipto;
136     if ($old) {
137       $custom_shipto = SL::DB::Shipto->new(
138         map  { +($_ => $old->$_) }
139         grep { !m{^ (?: itime | mtime | shipto_id | trans_id ) $}x }
140         map  { $_->name }
141         @{ $old->meta->columns }
142       );
143       $custom_shipto->module('DO');
144     }
145
146   } else {
147     $args{shipto_id} = $source->shipto_id;
148   }
149
150   my $delivery_order = $class->new(%args);
151   $delivery_order->assign_attributes(%{ $params{attributes} }) if $params{attributes};
152   my $items          = delete($params{items}) || $source->items_sorted;
153   my %item_parents;
154
155   my @items = map {
156     my $source_item      = $_;
157     my $source_item_id   = $_->$item_parent_id_column;
158     my @custom_variables = map { _clone_orderitem_cvar($_) } @{ $source_item->custom_variables };
159
160     $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
161     my $item_parent                  = $item_parents{$source_item_id};
162
163     SL::DB::DeliveryOrderItem->new(map({ ( $_ => $source_item->$_ ) }
164                                          qw(base_qty cusordnumber description discount lastcost longdescription marge_price_factor parts_id price_factor price_factor_id
165                                             project_id qty reqdate sellprice serialnumber transdate unit active_discount_source active_price_source
166                                          )),
167                                    custom_variables => \@custom_variables,
168                                    ordnumber        => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
169                                  );
170
171   } @{ $items };
172
173   @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
174   @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
175   @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
176
177   $delivery_order->items(\@items);
178
179   return ($delivery_order, $custom_shipto);
180 }
181
182 sub customervendor {
183   $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
184 }
185
186 1;
187 __END__
188
189 =pod
190
191 =encoding utf8
192
193 =head1 NAME
194
195 SL::DB::DeliveryOrder - Rose model for delivery orders (table
196 "delivery_orders")
197
198 =head1 FUNCTIONS
199
200 =over 4
201
202 =item C<date>
203
204 An alias for C<transdate> for compatibility with other sales/purchase models.
205
206 =item C<displayable_state>
207
208 Returns a human-readable description of the state regarding being
209 closed and delivered.
210
211 =item C<items>
212
213 An alias for C<deliver_orer_items> for compatibility with other
214 sales/purchase models.
215
216 =item C<new_from $source, %params>
217
218 Creates a new C<SL::DB::DeliveryOrder> instance and copies as much
219 information from C<$source> as possible. At the moment only instances
220 of C<SL::DB::Order> (sales quotations, sales orders, requests for
221 quotations and purchase orders) are supported as sources.
222
223 The conversion copies order items into delivery order items. Dates are copied
224 as appropriate, e.g. the C<transdate> field will be set to the current date.
225
226 Returns one or two objects depending on the context. In list context
227 the new delivery order instance and a shipto instance will be
228 returned. In scalar instance only the delivery order instance is
229 returned.
230
231 Custom shipto addresses (the ones specific to the sales/purchase
232 record and not to the customer/vendor) are only linked from C<shipto>
233 to C<delivery_orders>. Meaning C<delivery_orders.shipto_id> will not
234 be filled in that case. That's why a separate shipto object is created
235 and returned.
236
237 The objects returned are not saved.
238
239 C<%params> can include the following options:
240
241 =over 2
242
243 =item C<items>
244
245 An optional array reference of RDBO instances for the items to use. If
246 missing then the method C<items_sorted> will be called on
247 C<$source>. This option can be used to override the sorting, to
248 exclude certain positions or to add additional ones.
249
250 =item C<skip_items_negative_qty>
251
252 If trueish then items with a negative quantity are skipped. Items with
253 a quantity of 0 are not affected by this option.
254
255 =item C<skip_items_zero_qty>
256
257 If trueish then items with a quantity of 0 are skipped.
258
259 =item C<item_filter>
260
261 An optional code reference that is called for each item with the item
262 as its sole parameter. Items for which the code reference returns a
263 falsish value will be skipped.
264
265 =item C<attributes>
266
267 An optional hash reference. If it exists then it is passed to C<new>
268 allowing the caller to set certain attributes for the new delivery
269 order.
270
271 =back
272
273 =item C<sales_order>
274
275 TODO: Describe sales_order
276
277 =item C<type>
278
279 Returns a stringdescribing this record's type: either
280 C<sales_delivery_order> or C<purchase_delivery_order>.
281
282 =back
283
284 =head1 BUGS
285
286 Nothing here yet.
287
288 =head1 AUTHOR
289
290 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
291
292 =cut