1 package SL::DB::Invoice;
6 use List::Util qw(first);
8 use Rose::DB::Object::Helpers ();
9 use SL::DB::MetaSetup::Invoice;
10 use SL::DB::Manager::Invoice;
11 use SL::DB::Helper::Payment qw(:ALL);
12 use SL::DB::Helper::AttrHTML;
13 use SL::DB::Helper::AttrSorted;
14 use SL::DB::Helper::FlattenToForm;
15 use SL::DB::Helper::LinkedRecords;
16 use SL::DB::Helper::PriceTaxCalculator;
17 use SL::DB::Helper::PriceUpdater;
18 use SL::DB::Helper::TransNumberGenerator;
19 use SL::Locale::String qw(t8);
20 use SL::DB::CustomVariable;
22 __PACKAGE__->meta->add_relationship(
24 type => 'one to many',
25 class => 'SL::DB::InvoiceItem',
26 column_map => { id => 'trans_id' },
28 with_objects => [ 'part' ]
32 type => 'one to many',
33 class => 'SL::DB::Invoice',
34 column_map => { id => 'storno_id' },
36 sepa_export_items => {
37 type => 'one to many',
38 class => 'SL::DB::SepaExportItem',
39 column_map => { id => 'ar_id' },
40 manager_args => { with_objects => [ 'sepa_export' ] }
43 type => 'many to many',
44 map_class => 'SL::DB::SepaExportItem',
46 map_to => 'sepa_export',
50 class => 'SL::DB::Shipto',
51 column_map => { id => 'trans_id' },
52 query_args => [ module => 'AR' ],
55 type => 'one to many',
56 class => 'SL::DB::AccTransaction',
57 column_map => { id => 'trans_id' },
59 with_objects => [ 'chart' ],
60 sort_by => 'acc_trans_id ASC',
65 __PACKAGE__->meta->initialize;
67 __PACKAGE__->attr_html('notes');
68 __PACKAGE__->attr_sorted('items');
70 __PACKAGE__->before_save('_before_save_set_invnumber');
74 sub _before_save_set_invnumber {
77 $self->create_trans_number if !$self->invnumber;
84 sub items { goto &invoiceitems; }
85 sub add_items { goto &add_invoiceitems; }
86 sub record_number { goto &invnumber; };
89 # For compatibility with Order, DeliveryOrder
90 croak 'not an accessor' if @_ > 1;
94 # it is assumed, that ordnumbers are unique here.
95 sub first_order_by_ordnumber {
98 my $orders = SL::DB::Manager::Order->get_all(
100 ordnumber => $self->ordnumber,
105 return first { $_->is_type('sales_order') } @{ $orders };
108 sub abschlag_percentage {
110 my $order = $self->first_order_by_ordnumber or return;
111 my $order_amount = $order->netamount or return;
112 return $self->abschlag
113 ? $self->netamount / $order_amount
119 die 'not a setter method' if @_;
121 return ($self->amount || 0) - ($self->netamount || 0);
124 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
128 return $self->paid >= $self->amount;
131 sub _clone_orderitem_delivery_order_item_cvar {
134 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
135 $cloned->sub_module('invoice');
141 my ($class, $source, %params) = @_;
143 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
144 croak("Cannot create invoices for purchase records") unless $source->customer_id;
146 require SL::DB::Employee;
148 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
150 if (ref($source) eq 'SL::DB::Order') {
151 @columns = qw(quonumber delivery_customer_id delivery_vendor_id);
152 @item_columns = qw(subtotal);
154 $item_parent_id_column = 'trans_id';
155 $item_parent_column = 'order';
158 @columns = qw(donumber);
160 $item_parent_id_column = 'delivery_order_id';
161 $item_parent_column = 'delivery_order';
164 my $terms = $source->can('payment_id') ? $source->payment_terms : undef;
166 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
167 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id payment_id), @columns),
168 transdate => DateTime->today_local,
169 gldate => DateTime->today_local,
170 duedate => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local,
175 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
178 if ($source->type =~ /_order$/) {
179 $args{deliverydate} = $source->reqdate;
180 $args{orddate} = $source->transdate;
182 $args{quodate} = $source->transdate;
185 my $invoice = $class->new(%args);
186 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
187 my $items = delete($params{items}) || $source->items_sorted;
191 my $source_item = $_;
192 my $source_item_id = $_->$item_parent_id_column;
193 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
195 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
196 my $item_parent = $item_parents{$source_item_id};
197 my $current_invoice_item =
198 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
199 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
200 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
201 deliverydate => $source_item->reqdate,
202 fxsellprice => $source_item->sellprice,
203 custom_variables => \@custom_variables,
204 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
205 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
208 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
209 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
210 $current_invoice_item;
213 @items = grep { $params{item_filter}->($_) } @items if $params{item_filter};
214 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
215 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
217 $invoice->invoiceitems(\@items);
223 my ($self, %params) = @_;
225 require SL::DB::Chart;
226 if (!$params{ar_id}) {
227 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
230 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
231 $params{ar_id} = $chart->id;
235 my %data = $self->calculate_prices_and_taxes;
237 $self->_post_create_assemblyitem_entries($data{assembly_items});
240 $self->_post_add_acctrans($data{amounts_cogs});
241 $self->_post_add_acctrans($data{amounts});
242 $self->_post_add_acctrans($data{taxes});
244 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
246 $self->_post_update_allocated($data{allocated});
249 if ($self->db->in_transaction) {
251 } elsif (!$self->db->do_transaction($worker)) {
252 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
259 sub _post_add_acctrans {
260 my ($self, $entries) = @_;
262 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
265 require SL::DB::AccTransaction;
266 require SL::DB::Chart;
267 while (my ($chart_id, $spec) = each %{ $entries }) {
268 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
269 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
272 SL::DB::AccTransaction->new(trans_id => $self->id,
273 chart_id => $chart_id,
274 amount => $spec->{amount},
275 tax_id => $spec->{tax_id},
276 taxkey => $spec->{taxkey},
277 project_id => $self->globalproject_id,
278 transdate => $self->transdate,
279 chart_link => $chart_link)->save;
283 sub _post_create_assemblyitem_entries {
284 my ($self, $assembly_entries) = @_;
286 my $items = $self->invoiceitems;
290 foreach my $item (@{ $items }) {
291 next if $item->assemblyitem;
293 push @new_items, $item;
296 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
297 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
298 description => $assembly_item->{part}->description,
299 unit => $assembly_item->{part}->unit,
300 qty => $assembly_item->{qty},
301 allocated => $assembly_item->{allocated},
304 assemblyitem => 't');
308 $self->invoiceitems(\@new_items);
311 sub _post_update_allocated {
312 my ($self, $allocated) = @_;
314 while (my ($invoice_id, $diff) = each %{ $allocated }) {
315 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
316 where => [ id => $invoice_id ]);
323 return 'ar_transaction' if !$self->invoice;
324 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
325 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
326 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
330 sub displayable_state {
333 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
336 sub displayable_type {
339 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
340 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
341 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
342 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
343 return t8('Invoice');
346 sub displayable_name {
347 join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
353 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
354 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
355 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
356 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
357 return t8('Invoice (one letter abbreviation)');
376 $html = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
377 $html = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
390 SL::DB::Invoice: Rose model for invoices (table "ar")
396 =item C<new_from $source, %params>
398 Creates a new C<SL::DB::Invoice> instance and copies as much
399 information from C<$source> as possible. At the moment only sales
400 orders and sales quotations are supported as sources.
402 The conversion copies order items into invoice items. Dates are copied
403 as appropriate, e.g. the C<transdate> field from an order will be
404 copied into the invoice's C<orddate> field.
406 C<%params> can include the following options:
412 An optional array reference of RDBO instances for the items to use. If
413 missing then the method C<items_sorted> will be called on
414 C<$source>. This option can be used to override the sorting, to
415 exclude certain positions or to add additional ones.
417 =item C<skip_items_negative_qty>
419 If trueish then items with a negative quantity are skipped. Items with
420 a quantity of 0 are not affected by this option.
422 =item C<skip_items_zero_qty>
424 If trueish then items with a quantity of 0 are skipped.
428 An optional code reference that is called for each item with the item
429 as its sole parameter. Items for which the code reference returns a
430 falsish value will be skipped.
434 An optional hash reference. If it exists then it is passed to C<new>
435 allowing the caller to set certain attributes for the new invoice.
436 For example to set a different transdate (default is the current date),
437 call the method like this:
440 $params{attributes}{transdate} = '28.08.2015';
441 $invoice = SL::DB::Invoice->new_from($self, %params)->post || die;
445 Amounts, prices and taxes are not
446 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
447 can be used for this.
449 The object returned is not saved.
451 =item C<post %params>
453 Posts the invoice. Required parameters are:
459 The ID of the accounds receivable chart the invoices amounts are
460 posted to. If it is not set then the first chart configured for
461 accounts receivables is used.
465 This function implements several steps:
469 =item 1. It calculates all prices, amounts and taxes by calling
470 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
472 =item 2. A new and unique invoice number is created.
474 =item 3. All amounts for costs of goods sold are recorded in
477 =item 4. All amounts for parts, services and assemblies are recorded
478 in C<acc_trans> with their respective charts. This is determined by
479 the part's buchungsgruppen.
481 =item 5. The total amount is posted to the accounts receivable chart
482 and recorded in C<acc_trans>.
484 =item 6. Items in C<invoice> are updated according to their allocation
485 status (regarding for costs of goold sold). Will only be done if
486 kivitendo is not configured to use Einnahmenüberschussrechnungen.
488 =item 7. The invoice and its items are saved.
492 Returns C<$self> on success and C<undef> on failure. The whole process
493 is run inside a transaction. If it fails then nothing is saved to or
494 changed in the database. A new transaction is only started if none is
497 =item C<basic_info $field>
499 See L<SL::DB::Object::basic_info>.
504 As explained in the new_from example, it is possible to set transdate to a new value.
505 From a user / programm point of view transdate is more than holy and there should be
506 some validity checker available for controller code. At least the same logic like in
507 Form.pm from ar.pl should be available:
508 # see old stuff ar.pl post
509 #$form->error($locale->text('Cannot post transaction above the maximum future booking date!'))
510 # if ($form->date_max_future($transdate, \%myconfig));
511 #$form->error($locale->text('Cannot post transaction for a closed period!')) if ($form->date_closed($form->{"transdate"}, \%myconfig));
515 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>