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;
21 __PACKAGE__->meta->add_relationship(
23 type => 'one to many',
24 class => 'SL::DB::InvoiceItem',
25 column_map => { id => 'trans_id' },
27 with_objects => [ 'part' ]
31 type => 'one to many',
32 class => 'SL::DB::Invoice',
33 column_map => { id => 'storno_id' },
35 sepa_export_items => {
36 type => 'one to many',
37 class => 'SL::DB::SepaExportItem',
38 column_map => { id => 'ar_id' },
39 manager_args => { with_objects => [ 'sepa_export' ] }
43 class => 'SL::DB::Shipto',
44 column_map => { id => 'trans_id' },
45 query_args => [ module => 'AR' ],
49 __PACKAGE__->meta->initialize;
51 __PACKAGE__->before_save('_before_save_set_invnumber');
55 sub _before_save_set_invnumber {
58 $self->create_trans_number if !$self->invnumber;
65 sub items { goto &invoiceitems; }
66 sub add_items { goto &add_invoiceitems; }
71 return [ sort {$a->id <=> $b->id } @{ $self->items } ];
75 # For compatibility with Order, DeliveryOrder
76 croak 'not an accessor' if @_ > 1;
80 # it is assumed, that ordnumbers are unique here.
81 sub first_order_by_ordnumber {
84 my $orders = SL::DB::Manager::Order->get_all(
86 ordnumber => $self->ordnumber,
91 return first { $_->is_type('sales_order') } @{ $orders };
94 sub abschlag_percentage {
96 my $order = $self->first_order_by_ordnumber or return;
97 my $order_amount = $order->netamount or return;
98 return $self->abschlag
99 ? $self->netamount / $order_amount
105 die 'not a setter method' if @_;
107 return ($self->amount || 0) - ($self->netamount || 0);
110 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
114 return $self->paid >= $self->amount;
117 sub _clone_orderitem_delivery_order_item_cvar {
120 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
121 $cloned->sub_module('invoice');
127 my ($class, $source, %params) = @_;
129 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
130 croak("Cannot create invoices for purchase records") unless $source->customer_id;
132 require SL::DB::Employee;
134 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms
135 : $source->customer_id ? $source ->customer->payment_terms
138 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
140 if (ref($source) eq 'SL::DB::Order') {
141 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
142 @item_columns = qw(subtotal);
144 $item_parent_id_column = 'trans_id';
145 $item_parent_column = 'order';
148 @columns = qw(donumber);
150 $item_parent_id_column = 'delivery_order_id';
151 $item_parent_column = 'delivery_order';
154 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
155 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
156 transdate => DateTime->today_local,
157 gldate => DateTime->today_local,
158 duedate => DateTime->today_local->add(days => ($terms ? $terms->terms_netto * 1 : 1)),
159 payment_id => $terms ? $terms->id : undef,
164 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
167 if ($source->type =~ /_order$/) {
168 $args{deliverydate} = $source->reqdate;
169 $args{orddate} = $source->transdate;
171 $args{quodate} = $source->transdate;
174 my $invoice = $class->new(%args);
175 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
176 my $items = delete($params{items}) || $source->items_sorted;
180 my $source_item = $_;
181 my $source_item_id = $_->$item_parent_id_column;
182 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
184 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
185 my $item_parent = $item_parents{$source_item_id};
187 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
188 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
189 base_qty longdescription lastcost price_factor_id), @item_columns),
190 deliverydate => $source_item->reqdate,
191 fxsellprice => $source_item->sellprice,
192 custom_variables => \@custom_variables,
193 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
194 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
199 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
201 $invoice->invoiceitems(\@items);
207 my ($self, %params) = @_;
209 require SL::DB::Chart;
210 if (!$params{ar_id}) {
211 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
214 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
215 $params{ar_id} = $chart->id;
219 my %data = $self->calculate_prices_and_taxes;
221 $self->_post_create_assemblyitem_entries($data{assembly_items});
224 $self->_post_add_acctrans($data{amounts_cogs});
225 $self->_post_add_acctrans($data{amounts});
226 $self->_post_add_acctrans($data{taxes});
228 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
230 $self->_post_update_allocated($data{allocated});
233 if ($self->db->in_transaction) {
235 } elsif (!$self->db->do_transaction($worker)) {
236 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
243 sub _post_add_acctrans {
244 my ($self, $entries) = @_;
246 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
249 require SL::DB::AccTransaction;
250 require SL::DB::Chart;
251 while (my ($chart_id, $spec) = each %{ $entries }) {
252 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
253 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
256 SL::DB::AccTransaction->new(trans_id => $self->id,
257 chart_id => $chart_id,
258 amount => $spec->{amount},
259 tax_id => $spec->{tax_id},
260 taxkey => $spec->{taxkey},
261 project_id => $self->globalproject_id,
262 transdate => $self->transdate,
263 chart_link => $chart_link)->save;
267 sub _post_create_assemblyitem_entries {
268 my ($self, $assembly_entries) = @_;
270 my $items = $self->invoiceitems;
274 foreach my $item (@{ $items }) {
275 next if $item->assemblyitem;
277 push @new_items, $item;
280 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
281 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
282 description => $assembly_item->{part}->description,
283 unit => $assembly_item->{part}->unit,
284 qty => $assembly_item->{qty},
285 allocated => $assembly_item->{allocated},
288 assemblyitem => 't');
292 $self->invoiceitems(\@new_items);
295 sub _post_update_allocated {
296 my ($self, $allocated) = @_;
298 while (my ($invoice_id, $diff) = each %{ $allocated }) {
299 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
300 where => [ id => $invoice_id ]);
307 return 'ar_transaction' if !$self->invoice;
308 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
309 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
310 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
314 sub displayable_state {
317 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
327 return unless $self->id;
329 require SL::DB::AccTransaction;
330 SL::DB::Manager::AccTransaction->get_all(query => [ trans_id => $self->id ]);
341 SL::DB::Invoice: Rose model for invoices (table "ar")
347 =item C<new_from $source, %params>
349 Creates a new C<SL::DB::Invoice> instance and copies as much
350 information from C<$source> as possible. At the moment only sales
351 orders and sales quotations are supported as sources.
353 The conversion copies order items into invoice items. Dates are copied
354 as appropriate, e.g. the C<transdate> field from an order will be
355 copied into the invoice's C<orddate> field.
357 C<%params> can include the following options:
363 An optional array reference of RDBO instances for the items to use. If
364 missing then the method C<items_sorted> will be called on
365 C<$source>. This option can be used to override the sorting, to
366 exclude certain positions or to add additional ones.
368 =item C<skip_items_zero_qty>
370 If trueish then items with a quantity of 0 are skipped.
374 An optional hash reference. If it exists then it is passed to C<new>
375 allowing the caller to set certain attributes for the new delivery
380 Amounts, prices and taxes are not
381 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
382 can be used for this.
384 The object returned is not saved.
386 =item C<post %params>
388 Posts the invoice. Required parameters are:
394 The ID of the accounds receivable chart the invoices amounts are
395 posted to. If it is not set then the first chart configured for
396 accounts receivables is used.
400 This function implements several steps:
404 =item 1. It calculates all prices, amounts and taxes by calling
405 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
407 =item 2. A new and unique invoice number is created.
409 =item 3. All amounts for costs of goods sold are recorded in
412 =item 4. All amounts for parts, services and assemblies are recorded
413 in C<acc_trans> with their respective charts. This is determined by
414 the part's buchungsgruppen.
416 =item 5. The total amount is posted to the accounts receivable chart
417 and recorded in C<acc_trans>.
419 =item 6. Items in C<invoice> are updated according to their allocation
420 status (regarding for costs of goold sold). Will only be done if
421 kivitendo is not configured to use Einnahmenüberschussrechnungen.
423 =item 7. The invoice and its items are saved.
427 Returns C<$self> on success and C<undef> on failure. The whole process
428 is run inside a transaction. If it fails then nothing is saved to or
429 changed in the database. A new transaction is only started if none is
432 =item C<basic_info $field>
434 See L<SL::DB::Object::basic_info>.
440 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>