1 package SL::DB::Reclamation;
8 use List::Util qw(max sum0);
9 use List::MoreUtils qw(any);
11 use SL::DB::Order::TypeData qw(:types);
12 use SL::DB::DeliveryOrder::TypeData qw(:types);
13 use SL::DB::Reclamation::TypeData qw(:types);
14 use SL::DB::MetaSetup::Reclamation;
15 use SL::DB::Manager::Reclamation;
16 use SL::DB::Helper::Attr;
17 use SL::DB::Helper::AttrHTML;
18 use SL::DB::Helper::AttrSorted;
19 use SL::DB::Helper::FlattenToForm;
20 use SL::DB::Helper::LinkedRecords;
21 use SL::DB::Helper::PriceTaxCalculator;
22 use SL::DB::Helper::PriceUpdater;
23 use SL::DB::Helper::TypeDataProxy;
24 use SL::DB::Helper::TransNumberGenerator;
25 use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF);
26 use SL::Locale::String qw(t8);
28 use Rose::DB::Object::Helpers qw(as_tree strip);
29 use SL::DB::Helper::LegacyPrinting qw(map_keys_to_arrays format_as_number);
31 __PACKAGE__->meta->add_relationship(
33 reclamation_items => {
34 type => 'one to many',
35 class => 'SL::DB::ReclamationItem',
36 column_map => { id => 'reclamation_id' },
38 with_objects => [ 'part', 'reason' ]
43 class => 'SL::DB::Shipto',
44 column_map => { id => 'trans_id' },
45 query_args => [ module => 'Reclamation' ],
49 class => 'SL::DB::Exchangerate',
50 column_map => { currency_id => 'currency_id', transdate => 'transdate' },
54 SL::DB::Helper::Attr::make(__PACKAGE__, daily_exchangerate => 'numeric');
56 __PACKAGE__->meta->initialize;
58 __PACKAGE__->attr_html('notes');
59 __PACKAGE__->attr_sorted('items');
61 __PACKAGE__->before_save('_before_save_set_record_number');
62 __PACKAGE__->before_save('_before_save_remove_empty_custom_shipto');
63 __PACKAGE__->before_save('_before_save_set_custom_shipto_module');
64 __PACKAGE__->after_save('_after_save_link_records');
68 sub _before_save_set_record_number {
71 $self->create_trans_number if !$self->record_number;
76 sub _before_save_remove_empty_custom_shipto {
79 $self->custom_shipto(undef) if $self->custom_shipto && $self->custom_shipto->is_empty;
84 sub _before_save_set_custom_shipto_module {
87 $self->custom_shipto->module('Reclamation') if $self->custom_shipto;
92 sub _after_save_link_records {
95 my @allowed_record_sources = qw(SL::DB::Reclamation SL::DB::Order SL::DB::DeliveryOrder SL::DB::Invoice SL::DB::PurchaseInvoice);
96 my @allowed_item_sources = qw(SL::DB::ReclamationItem SL::DB::OrderItem SL::DB::DeliveryOrderItem SL::DB::InvoiceItem);
98 SL::DB::Helper::RecordLink::link_records(
100 \@allowed_record_sources,
101 \@allowed_item_sources,
107 sub items { goto &reclamation_items; }
108 sub add_items { goto &add_reclamation_items; }
109 sub record_items { goto &reclamation_items; }
113 die "invalid type: " . $self->record_type if (!any { $self->record_type eq $_ } (
114 SALES_RECLAMATION_TYPE(),
115 PURCHASE_RECLAMATION_TYPE(),
117 return $self->record_type;
121 my ($self, $type) = @_;
122 return $self->type eq $type;
125 sub effective_tax_point {
128 return $self->tax_point || $self->reqdate || $self->transdate;
131 sub displayable_type {
133 return $self->type_data->text('type');
136 sub displayable_name {
137 join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
141 croak 'not an accessor' if @_ > 1;
142 $_[0]->type_data->properties('is_customer');
145 sub daily_exchangerate {
146 my ($self, $val) = @_;
148 return 1 if $self->currency_id == $::instance_conf->get_currency_id;
150 my $rate = (any { $self->is_type($_) } (SALES_RECLAMATION_TYPE())) ? 'buy'
151 : (any { $self->is_type($_) } (PURCHASE_RECLAMATION_TYPE())) ? 'sell'
156 croak t8('exchange rate has to be positive') if $val <= 0;
157 if (!$self->exchangerate_obj) {
158 $self->exchangerate_obj(SL::DB::Exchangerate->new(
159 currency_id => $self->currency_id,
160 transdate => $self->transdate,
163 } elsif (!defined $self->exchangerate_obj->$rate) {
164 $self->exchangerate_obj->$rate($val);
166 croak t8('exchange rate already exists, no update allowed');
169 return $self->exchangerate_obj->$rate if $self->exchangerate_obj;
174 # add taxes to recalmation
175 my %pat = $self->calculate_prices_and_taxes();
177 foreach my $tax_id (keys %{ $pat{taxes_by_tax_id} }) {
178 my $netamount = sum0 map { $pat{amounts}->{$_}->{amount} } grep { $pat{amounts}->{$_}->{tax_id} == $tax_id } keys %{ $pat{amounts} };
179 push(@taxes, { amount => $pat{taxes_by_tax_id}->{$tax_id},
180 netamount => $netamount,
181 tax => SL::DB::Tax->new(id => $tax_id)->load });
186 sub displayable_state {
189 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
192 sub valid_reclamation_reasons {
195 my $valid_for_type = ($self->type =~ m{sales} ? 'valid_for_sales' : 'valid_for_purchase');
196 return SL::DB::Manager::ReclamationReason->get_all_sorted(
197 where => [ $valid_for_type => 1 ]);
200 sub convert_to_order {
201 my ($self, %params) = @_;
204 $params{destination_type} = $self->is_sales ? SALES_ORDER_TYPE()
205 : PURCHASE_ORDER_TYPE();
206 if (!$self->db->with_transaction(sub {
207 require SL::DB::Order;
208 $order = SL::DB::Order->new_from($self, %params);
213 return undef, $self->db->error->db_error->db_error;
219 sub convert_to_delivery_order {
220 my ($self, %params) = @_;
223 if (!$self->db->with_transaction(sub {
224 require SL::DB::DeliveryOrder;
225 $delivery_order = SL::DB::DeliveryOrder->new_from($self, %params);
226 $delivery_order->save;
228 $self->update_attributes(delivered => 1) unless $::instance_conf->get_shipped_qty_require_stock_out;
231 return undef, $self->db->error->db_error->db_error;
234 return $delivery_order;
237 sub add_legacy_template_arrays {
238 my ($self, $print_form) = @_;
240 # for now using the keys that are used in the latex template: template/print/marei/sales_reclamation.tex
241 # (nested keys: part.partnumber, reason.description)
242 my @keys = qw( position part.partnumber description longdescription reqdate serialnumber projectnumber reason.description
243 reason_description_ext qty_as_number unit sellprice_as_number discount_as_number discount_as_percent linetotal );
245 my @tax_keys = qw( tax.taxdescription amount );
248 map_keys_to_arrays($self->items_sorted, \@keys, \%template_arrays);
249 map_keys_to_arrays($self->taxes, \@tax_keys, \%template_arrays);
251 format_as_number([ qw(linetotal) ], \%template_arrays);
252 $print_form->{TEMPLATE_ARRAYS} = \%template_arrays;
255 #TODO(Werner): überprüfen ob alle Felder richtig gestetzt werden
257 my ($class, $source, %params) = @_;
258 my %allowed_sources = map { $_ => 1 } qw(
261 SL::DB::DeliveryOrder
263 SL::DB::PurchaseInvoice
265 unless( $allowed_sources{ref $source} ) {
266 croak("Unsupported source object type '" . ref($source) . "'");
268 croak("A destination type must be given as parameter") unless $params{destination_type};
270 my $destination_type = delete $params{destination_type};
274 { from => SALES_RECLAMATION_TYPE(), to => SALES_RECLAMATION_TYPE(), abbr => 'srsr', },
275 { from => PURCHASE_RECLAMATION_TYPE(), to => PURCHASE_RECLAMATION_TYPE(), abbr => 'prpr', },
276 { from => SALES_RECLAMATION_TYPE(), to => PURCHASE_RECLAMATION_TYPE(), abbr => 'srpr', },
277 { from => PURCHASE_RECLAMATION_TYPE(), to => SALES_RECLAMATION_TYPE(), abbr => 'prsr', },
279 { from => SALES_ORDER_TYPE(), to => SALES_RECLAMATION_TYPE(), abbr => 'sosr', },
280 { from => PURCHASE_ORDER_TYPE(), to => PURCHASE_RECLAMATION_TYPE(), abbr => 'popr', },
282 { from => SALES_DELIVERY_ORDER_TYPE(), to => SALES_RECLAMATION_TYPE(), abbr => 'sdsr', },
283 { from => PURCHASE_DELIVERY_ORDER_TYPE(), to => PURCHASE_RECLAMATION_TYPE(), abbr => 'pdpr', },
285 { from => 'invoice', to => SALES_RECLAMATION_TYPE(), abbr => 'sisr', },
286 { from => 'purchase_invoice', to => PURCHASE_RECLAMATION_TYPE(), abbr => 'pipr', },
288 my $from_to = (grep { $_->{from} eq $source->record_type && $_->{to} eq $destination_type} @from_tos)[0];
290 croak("Cannot convert from '" . $source->record_type . "' to '" . $destination_type . "'");
293 my $is_abbr_any = sub {
294 any { $from_to->{abbr} eq $_ } @_;
298 record_number => undef,
299 record_type => $destination_type,
300 employee => SL::DB::Manager::Employee->current,
303 transdate => DateTime->today_local,
305 if ( $is_abbr_any->(qw(srsr prpr srpr prsr)) ) { #Reclamation
306 map { $record_args{$_} = $source->$_ } # {{{ for vim folds
330 transaction_description
332 ); # }}} for vim folds
333 } elsif ( $is_abbr_any->(qw(sosr popr)) ) { #Order
334 map { $record_args{$_} = $source->$_ } # {{{ for vim folds
355 transaction_description
358 $record_args{contact_id} = $source->cp_id;
359 $record_args{cv_record_number} = $source->cusordnumber;
361 } elsif ( $is_abbr_any->(qw(sdsr pdpr)) ) { #DeliveryOrder
362 map { $record_args{$_} = $source->$_ } # {{{ for vim folds
380 transaction_description
383 $record_args{contact_id} = $source->cp_id;
384 $record_args{cv_record_number} = $source->cusordnumber;
386 } elsif ( $is_abbr_any->(qw(sisr)) ) { #Invoice(ar)
387 map { $record_args{$_} = $source->$_ } # {{{ for vim folds
407 transaction_description
409 $record_args{contact_id} = $source->cp_id;
410 $record_args{cv_record_number} = $source->cusordnumber;
412 } elsif ( $is_abbr_any->(qw(pipr)) ) { #Invoice(ap)
413 map { $record_args{$_} = $source->$_ } # {{{ for vim folds
429 transaction_description
432 $record_args{contact_id} = $source->cp_id;
436 if ( ($from_to->{from} =~ m{sales}) && ($from_to->{to} =~ m{purchase}) ) {
437 $record_args{customer_id} = undef;
438 $record_args{billing_address_id} = undef;
439 $record_args{salesman_id} = undef;
440 $record_args{payment_id} = undef;
441 $record_args{delivery_term_id} = undef;
443 if ( ($from_to->{from} =~ m{purchase}) && ($from_to->{to} =~ m{sales}) ) {
444 $record_args{vendor_id} = undef;
445 $record_args{salesman_id} = undef;
446 $record_args{payment_id} = undef;
447 $record_args{delivery_term_id} = undef;
451 if ($source->can('shipto_id')) {
452 # Custom shipto addresses (the ones specific to the sales/purchase record and
453 # not to the customer/vendor) are only linked from shipto → record.
454 # Meaning record.shipto_id will not be filled in that case.
455 if (!$source->shipto_id && $source->id) {
456 $record_args{custom_shipto} = $source->custom_shipto->clone($class) if $source->can('custom_shipto') && $source->custom_shipto;
457 } elsif ($source->shipto_id) {
458 $record_args{shipto_id} = $source->shipto_id;
462 my $reclamation = $class->new(%record_args);
463 $reclamation->assign_attributes(%{ $params{attributes} }) if $params{attributes};
465 unless ($params{no_linked_records}) {
466 $reclamation->{RECORD_TYPE_REF()} = ref($source);
467 $reclamation->{RECORD_ID()} = $source->id;
470 my $items = delete($params{items}) || $source->items;
472 my @items = map { SL::DB::ReclamationItem->new_from($_, $from_to->{to}, no_linked_records => $params{no_linked_records}); } @{ $items };
474 @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
475 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
476 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
478 $reclamation->items(\@items);
483 my ($reclamation) = @_;
484 return $reclamation->is_sales ? $reclamation->customer : $reclamation->vendor;
494 sprintf "%s %s %s (%s)",
495 $self->record_number,
496 $self->customervendor->name,
497 $self->amount_as_number,
498 $self->date->to_kivitendo;
502 SL::DB::Helper::TypeDataProxy->new(ref $_[0], $_[0]->type);
516 SL::DB::Reclamation - reclamation Datenbank Objekt.
522 Returns one of the following string types:
526 =item sales_reclamation
528 =item purchase_reclamation
530 =item sales_quotation
532 =item request_quotation
536 =head2 C<is_type TYPE>
538 Returns true if the reclamation is of the given type.
540 =head2 C<daily_exchangerate $val>
542 Gets or sets the exchangerate object's value. This is the value from the
543 table C<exchangerate> depending on the reclamation's currency, the transdate and
544 if it is a sales or purchase reclamation.
546 The reclamation object (respectively the table C<oe>) has an own column
547 C<exchangerate> which can be get or set with the accessor C<exchangerate>.
549 The idea is to drop the legacy table C<exchangerate> in the future and to
550 give all relevant tables it's own C<exchangerate> column.
552 So, this method is here if you need to access the "legacy" exchangerate via
553 an reclamation object.
559 (optional) If given, the exchangerate in the "legacy" table is set to this
560 value, depending on currency, transdate and sales or purchase.
564 =head2 C<convert_to_delivery_order %params>
566 Creates a new delivery reclamation with C<$self> as the basis by calling
567 L<SL::DB::DeliveryReclamation::new_from>. That delivery reclamation is saved, and
568 C<$self> is linked to the new invoice via
569 L<SL::DB::RecordLink>. C<$self>'s C<delivered> attribute is set to
570 C<true>, and C<$self> is saved.
572 The arguments in C<%params> are passed to
573 L<SL::DB::DeliveryReclamation::new_from>.
575 Returns C<undef> on failure. Otherwise the new delivery reclamation will be
578 =head2 C<convert_to_invoice %params>
580 Creates a new invoice with C<$self> as the basis by calling
581 L<SL::DB::Invoice::new_from>. That invoice is posted, and C<$self> is
582 linked to the new invoice via L<SL::DB::RecordLink>. C<$self>'s
583 C<closed> attribute is set to C<true>, and C<$self> is saved.
585 The arguments in C<%params> are passed to L<SL::DB::Invoice::post>.
587 Returns the new invoice instance on success and C<undef> on
588 failure. The whole process is run inside a transaction. On failure
589 nothing is created or changed in the database.
591 At the moment only sales quotations and sales reclamations can be converted.
593 =head2 C<add_legacy_template_arrays $print_form>
595 For printing OpenDocument documents we need to extract loop variables (items and
596 taxes) from the Rose DB object and add them to the form, in the format that the
597 built-in template parser expects.
599 <$print_form> Print form used in the controller.
601 =head2 C<new_from $source, %params>
603 Creates a new C<SL::DB::Reclamation> instance and copies as much
604 information from C<$source> as possible. At the moment only records with the
605 same destination type as the source type and sales reclamations from
606 sales quotations and purchase reclamations from requests for quotations can be
609 The C<transdate> field will be set to the current date.
611 The conversion copies the reclamation items as well.
613 Returns the new reclamation instance. The object returned is not
616 C<%params> can include the following options
617 (C<destination_type> is mandatory):
621 =item C<destination_type>
624 The type of the newly created object. Can be C<sales_quotation>,
625 C<sales_reclamation>, C<purchase_quotation> or C<purchase_reclamation> for now.
629 An optional array reference of RDBO instances for the items to use. If
630 missing then the method C<items_sorted> will be called on
631 C<$source>. This option can be used to override the sorting, to
632 exclude certain positions or to add additional ones.
634 =item C<skip_items_negative_qty>
636 If trueish then items with a negative quantity are skipped. Items with
637 a quantity of 0 are not affected by this option.
639 =item C<skip_items_zero_qty>
641 If trueish then items with a quantity of 0 are skipped.
645 An optional code reference that is called for each item with the item
646 as its sole parameter. Items for which the code reference returns a
647 falsish value will be skipped.
651 An optional hash reference. If it exists then it is passed to C<new>
652 allowing the caller to set certain attributes for the new delivery
663 Sven Schöling <s.schoeling@linet-services.de>