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::FlattenToForm;
16 use SL::DB::Helper::LinkedRecords;
17 use SL::DB::Helper::PriceTaxCalculator;
18 use SL::DB::Helper::PriceUpdater;
19 use SL::DB::Helper::TransNumberGenerator;
20 use SL::Locale::String qw(t8);
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' ] }
44 class => 'SL::DB::Shipto',
45 column_map => { id => 'trans_id' },
46 query_args => [ module => 'AR' ],
50 __PACKAGE__->meta->initialize;
52 __PACKAGE__->before_save('_before_save_set_invnumber');
56 sub _before_save_set_invnumber {
59 $self->create_trans_number if !$self->invnumber;
66 sub items { goto &invoiceitems; }
67 sub add_items { goto &add_invoiceitems; }
72 return [ sort {$a->id <=> $b->id } @{ $self->items } ];
76 # For compatibility with Order, DeliveryOrder
77 croak 'not an accessor' if @_ > 1;
81 # it is assumed, that ordnumbers are unique here.
82 sub first_order_by_ordnumber {
85 my $orders = SL::DB::Manager::Order->get_all(
87 ordnumber => $self->ordnumber,
92 return first { $_->is_type('sales_order') } @{ $orders };
95 sub abschlag_percentage {
97 my $order = $self->first_order_by_ordnumber or return;
98 my $order_amount = $order->netamount or return;
99 return $self->abschlag
100 ? $self->netamount / $order_amount
106 die 'not a setter method' if @_;
108 return ($self->amount || 0) - ($self->netamount || 0);
111 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
115 return $self->paid >= $self->amount;
118 sub _clone_orderitem_delivery_order_item_cvar {
121 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
122 $cloned->sub_module('invoice');
128 my ($class, $source, %params) = @_;
130 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
131 croak("Cannot create invoices for purchase records") unless $source->customer_id;
133 require SL::DB::Employee;
135 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
136 : $source->customer_id ? $source ->customer->payment_terms
139 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
141 if (ref($source) eq 'SL::DB::Order') {
142 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
143 @item_columns = qw(subtotal);
145 $item_parent_id_column = 'trans_id';
146 $item_parent_column = 'order';
149 @columns = qw(donumber);
151 $item_parent_id_column = 'delivery_order_id';
152 $item_parent_column = 'delivery_order';
155 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
156 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
157 transdate => DateTime->today_local,
158 gldate => DateTime->today_local,
159 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
160 payment_id => $terms ? $terms->id : undef,
165 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
168 if ($source->type =~ /_order$/) {
169 $args{deliverydate} = $source->reqdate;
170 $args{orddate} = $source->transdate;
172 $args{quodate} = $source->transdate;
175 my $invoice = $class->new(%args);
176 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
177 my $items = delete($params{items}) || $source->items_sorted;
181 my $source_item = $_;
182 my $source_item_id = $_->$item_parent_id_column;
183 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
185 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
186 my $item_parent = $item_parents{$source_item_id};
188 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
189 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
190 base_qty longdescription lastcost price_factor_id), @item_columns),
191 deliverydate => $source_item->reqdate,
192 fxsellprice => $source_item->sellprice,
193 custom_variables => \@custom_variables,
194 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
195 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
200 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
202 $invoice->invoiceitems(\@items);
208 my ($self, %params) = @_;
210 require SL::DB::Chart;
211 if (!$params{ar_id}) {
212 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
215 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
216 $params{ar_id} = $chart->id;
220 my %data = $self->calculate_prices_and_taxes;
222 $self->_post_create_assemblyitem_entries($data{assembly_items});
225 $self->_post_add_acctrans($data{amounts_cogs});
226 $self->_post_add_acctrans($data{amounts});
227 $self->_post_add_acctrans($data{taxes});
229 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
231 $self->_post_update_allocated($data{allocated});
234 if ($self->db->in_transaction) {
236 } elsif (!$self->db->do_transaction($worker)) {
237 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
244 sub _post_add_acctrans {
245 my ($self, $entries) = @_;
247 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
250 require SL::DB::AccTransaction;
251 require SL::DB::Chart;
252 while (my ($chart_id, $spec) = each %{ $entries }) {
253 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
254 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
257 SL::DB::AccTransaction->new(trans_id => $self->id,
258 chart_id => $chart_id,
259 amount => $spec->{amount},
260 tax_id => $spec->{tax_id},
261 taxkey => $spec->{taxkey},
262 project_id => $self->globalproject_id,
263 transdate => $self->transdate,
264 chart_link => $chart_link)->save;
268 sub _post_create_assemblyitem_entries {
269 my ($self, $assembly_entries) = @_;
271 my $items = $self->invoiceitems;
275 foreach my $item (@{ $items }) {
276 next if $item->assemblyitem;
278 push @new_items, $item;
281 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
282 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
283 description => $assembly_item->{part}->description,
284 unit => $assembly_item->{part}->unit,
285 qty => $assembly_item->{qty},
286 allocated => $assembly_item->{allocated},
289 assemblyitem => 't');
293 $self->invoiceitems(\@new_items);
296 sub _post_update_allocated {
297 my ($self, $allocated) = @_;
299 while (my ($invoice_id, $diff) = each %{ $allocated }) {
300 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
301 where => [ id => $invoice_id ]);
308 return 'ar_transaction' if !$self->invoice;
309 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
310 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
311 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
315 sub displayable_state {
318 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
324 return t8('AR Transaction (abbreviation)') if !$self->invoice;
325 return t8('Credit note (one letter abbreviation)') if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
326 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
327 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
328 return t8('Invoice (one letter abbreviation)');
339 return unless $self->id;
341 require SL::DB::AccTransaction;
342 SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
353 SL::DB::Invoice: Rose model for invoices (table "ar")
359 =item C<new_from $source, %params>
361 Creates a new C<SL::DB::Invoice> instance and copies as much
362 information from C<$source> as possible. At the moment only sales
363 orders and sales quotations are supported as sources.
365 The conversion copies order items into invoice items. Dates are copied
366 as appropriate, e.g. the C<transdate> field from an order will be
367 copied into the invoice's C<orddate> field.
369 C<%params> can include the following options:
375 An optional array reference of RDBO instances for the items to use. If
376 missing then the method C<items_sorted> will be called on
377 C<$source>. This option can be used to override the sorting, to
378 exclude certain positions or to add additional ones.
380 =item C<skip_items_zero_qty>
382 If trueish then items with a quantity of 0 are skipped.
386 An optional hash reference. If it exists then it is passed to C<new>
387 allowing the caller to set certain attributes for the new delivery
392 Amounts, prices and taxes are not
393 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
394 can be used for this.
396 The object returned is not saved.
398 =item C<post %params>
400 Posts the invoice. Required parameters are:
406 The ID of the accounds receivable chart the invoices amounts are
407 posted to. If it is not set then the first chart configured for
408 accounts receivables is used.
412 This function implements several steps:
416 =item 1. It calculates all prices, amounts and taxes by calling
417 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
419 =item 2. A new and unique invoice number is created.
421 =item 3. All amounts for costs of goods sold are recorded in
424 =item 4. All amounts for parts, services and assemblies are recorded
425 in C<acc_trans> with their respective charts. This is determined by
426 the part's buchungsgruppen.
428 =item 5. The total amount is posted to the accounts receivable chart
429 and recorded in C<acc_trans>.
431 =item 6. Items in C<invoice> are updated according to their allocation
432 status (regarding for costs of goold sold). Will only be done if
433 kivitendo is not configured to use Einnahmenüberschussrechnungen.
435 =item 7. The invoice and its items are saved.
439 Returns C<$self> on success and C<undef> on failure. The whole process
440 is run inside a transaction. If it fails then nothing is saved to or
441 changed in the database. A new transaction is only started if none is
444 =item C<basic_info $field>
446 See L<SL::DB::Object::basic_info>.
452 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>