1 # This file has been auto-generated only because it didn't exist.
2 # Feel free to modify it at will; it will not be overwritten automatically.
4 package SL::DB::Invoice;
9 use List::Util qw(first);
11 use Rose::DB::Object::Helpers ();
13 use SL::DB::MetaSetup::Invoice;
14 use SL::DB::Manager::Invoice;
15 use SL::DB::Helper::AttrHTML;
16 use SL::DB::Helper::FlattenToForm;
17 use SL::DB::Helper::LinkedRecords;
18 use SL::DB::Helper::PriceTaxCalculator;
19 use SL::DB::Helper::PriceUpdater;
20 use SL::DB::Helper::TransNumberGenerator;
21 use SL::Locale::String qw(t8);
23 __PACKAGE__->meta->add_relationship(
25 type => 'one to many',
26 class => 'SL::DB::InvoiceItem',
27 column_map => { id => 'trans_id' },
29 with_objects => [ 'part' ]
33 type => 'one to many',
34 class => 'SL::DB::Invoice',
35 column_map => { id => 'storno_id' },
37 sepa_export_items => {
38 type => 'one to many',
39 class => 'SL::DB::SepaExportItem',
40 column_map => { id => 'ar_id' },
41 manager_args => { with_objects => [ 'sepa_export' ] }
45 class => 'SL::DB::Shipto',
46 column_map => { id => 'trans_id' },
47 query_args => [ module => 'AR' ],
50 type => 'one to many',
51 class => 'SL::DB::AccTransaction',
52 column_map => { id => 'trans_id' },
54 with_objects => [ 'chart' ],
55 sort_by => 'acc_trans_id ASC',
60 __PACKAGE__->meta->initialize;
62 __PACKAGE__->attr_html('notes');
64 __PACKAGE__->before_save('_before_save_set_invnumber');
68 sub _before_save_set_invnumber {
71 $self->create_trans_number if !$self->invnumber;
78 sub items { goto &invoiceitems; }
79 sub add_items { goto &add_invoiceitems; }
84 return [ sort {$a->position <=> $b->position } @{ $self->items } ];
88 # For compatibility with Order, DeliveryOrder
89 croak 'not an accessor' if @_ > 1;
93 # it is assumed, that ordnumbers are unique here.
94 sub first_order_by_ordnumber {
97 my $orders = SL::DB::Manager::Order->get_all(
99 ordnumber => $self->ordnumber,
104 return first { $_->is_type('sales_order') } @{ $orders };
107 sub abschlag_percentage {
109 my $order = $self->first_order_by_ordnumber or return;
110 my $order_amount = $order->netamount or return;
111 return $self->abschlag
112 ? $self->netamount / $order_amount
118 die 'not a setter method' if @_;
120 return ($self->amount || 0) - ($self->netamount || 0);
123 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
127 return $self->paid >= $self->amount;
130 sub _clone_orderitem_delivery_order_item_cvar {
133 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
134 $cloned->sub_module('invoice');
140 my ($class, $source, %params) = @_;
142 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
143 croak("Cannot create invoices for purchase records") unless $source->customer_id;
145 require SL::DB::Employee;
147 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
148 : $source->customer_id ? $source ->customer->payment_terms
151 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
153 if (ref($source) eq 'SL::DB::Order') {
154 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
155 @item_columns = qw(subtotal);
157 $item_parent_id_column = 'trans_id';
158 $item_parent_column = 'order';
161 @columns = qw(donumber);
163 $item_parent_id_column = 'delivery_order_id';
164 $item_parent_column = 'delivery_order';
167 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
168 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
169 transdate => DateTime->today_local,
170 gldate => DateTime->today_local,
171 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
172 payment_id => $terms ? $terms->id : undef,
177 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
180 if ($source->type =~ /_order$/) {
181 $args{deliverydate} = $source->reqdate;
182 $args{orddate} = $source->transdate;
184 $args{quodate} = $source->transdate;
187 my $invoice = $class->new(%args);
188 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
189 my $items = delete($params{items}) || $source->items_sorted;
193 my $source_item = $_;
194 my $source_item_id = $_->$item_parent_id_column;
195 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
197 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
198 my $item_parent = $item_parents{$source_item_id};
199 my $current_invoice_item =
200 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
201 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
202 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
203 deliverydate => $source_item->reqdate,
204 fxsellprice => $source_item->sellprice,
205 custom_variables => \@custom_variables,
206 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
207 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
210 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
211 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
212 $current_invoice_item;
215 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
216 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
218 $invoice->invoiceitems(\@items);
224 my ($self, %params) = @_;
226 require SL::DB::Chart;
227 if (!$params{ar_id}) {
228 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
231 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
232 $params{ar_id} = $chart->id;
236 my %data = $self->calculate_prices_and_taxes;
238 $self->_post_create_assemblyitem_entries($data{assembly_items});
241 $self->_post_add_acctrans($data{amounts_cogs});
242 $self->_post_add_acctrans($data{amounts});
243 $self->_post_add_acctrans($data{taxes});
245 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
247 $self->_post_update_allocated($data{allocated});
250 if ($self->db->in_transaction) {
252 } elsif (!$self->db->do_transaction($worker)) {
253 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
260 sub _post_add_acctrans {
261 my ($self, $entries) = @_;
263 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
266 require SL::DB::AccTransaction;
267 require SL::DB::Chart;
268 while (my ($chart_id, $spec) = each %{ $entries }) {
269 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
270 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
273 SL::DB::AccTransaction->new(trans_id => $self->id,
274 chart_id => $chart_id,
275 amount => $spec->{amount},
276 tax_id => $spec->{tax_id},
277 taxkey => $spec->{taxkey},
278 project_id => $self->globalproject_id,
279 transdate => $self->transdate,
280 chart_link => $chart_link)->save;
284 sub _post_create_assemblyitem_entries {
285 my ($self, $assembly_entries) = @_;
287 my $items = $self->invoiceitems;
291 foreach my $item (@{ $items }) {
292 next if $item->assemblyitem;
294 push @new_items, $item;
297 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
298 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
299 description => $assembly_item->{part}->description,
300 unit => $assembly_item->{part}->unit,
301 qty => $assembly_item->{qty},
302 allocated => $assembly_item->{allocated},
305 assemblyitem => 't');
309 $self->invoiceitems(\@new_items);
312 sub _post_update_allocated {
313 my ($self, $allocated) = @_;
315 while (my ($invoice_id, $diff) = each %{ $allocated }) {
316 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
317 where => [ id => $invoice_id ]);
324 return 'ar_transaction' if !$self->invoice;
325 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
326 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
327 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
331 sub displayable_state {
334 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
337 sub displayable_type {
340 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
341 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
342 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
343 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
344 return t8('Invoice');
350 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
351 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
352 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
353 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
354 return t8('Invoice (one letter abbreviation)');
377 SL::DB::Invoice: Rose model for invoices (table "ar")
383 =item C<new_from $source, %params>
385 Creates a new C<SL::DB::Invoice> instance and copies as much
386 information from C<$source> as possible. At the moment only sales
387 orders and sales quotations are supported as sources.
389 The conversion copies order items into invoice items. Dates are copied
390 as appropriate, e.g. the C<transdate> field from an order will be
391 copied into the invoice's C<orddate> field.
393 C<%params> can include the following options:
399 An optional array reference of RDBO instances for the items to use. If
400 missing then the method C<items_sorted> will be called on
401 C<$source>. This option can be used to override the sorting, to
402 exclude certain positions or to add additional ones.
404 =item C<skip_items_negative_qty>
406 If trueish then items with a negative quantity are skipped. Items with
407 a quantity of 0 are not affected by this option.
409 =item C<skip_items_zero_qty>
411 If trueish then items with a quantity of 0 are skipped.
415 An optional hash reference. If it exists then it is passed to C<new>
416 allowing the caller to set certain attributes for the new delivery
421 Amounts, prices and taxes are not
422 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
423 can be used for this.
425 The object returned is not saved.
427 =item C<post %params>
429 Posts the invoice. Required parameters are:
435 The ID of the accounds receivable chart the invoices amounts are
436 posted to. If it is not set then the first chart configured for
437 accounts receivables is used.
441 This function implements several steps:
445 =item 1. It calculates all prices, amounts and taxes by calling
446 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
448 =item 2. A new and unique invoice number is created.
450 =item 3. All amounts for costs of goods sold are recorded in
453 =item 4. All amounts for parts, services and assemblies are recorded
454 in C<acc_trans> with their respective charts. This is determined by
455 the part's buchungsgruppen.
457 =item 5. The total amount is posted to the accounts receivable chart
458 and recorded in C<acc_trans>.
460 =item 6. Items in C<invoice> are updated according to their allocation
461 status (regarding for costs of goold sold). Will only be done if
462 kivitendo is not configured to use Einnahmenüberschussrechnungen.
464 =item 7. The invoice and its items are saved.
468 Returns C<$self> on success and C<undef> on failure. The whole process
469 is run inside a transaction. If it fails then nothing is saved to or
470 changed in the database. A new transaction is only started if none is
473 =item C<basic_info $field>
475 See L<SL::DB::Object::basic_info>.
481 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>