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; }
70 return [ sort {$a->id <=> $b->id } @{ $self->items } ];
74 # For compatibility with Order, DeliveryOrder
75 croak 'not an accessor' if @_ > 1;
79 # it is assumed, that ordnumbers are unique here.
80 sub first_order_by_ordnumber {
83 my $orders = SL::DB::Manager::Order->get_all(
85 ordnumber => $self->ordnumber,
90 return first { $_->is_type('sales_order') } @{ $orders };
93 sub abschlag_percentage {
95 my $order = $self->first_order_by_ordnumber or return;
96 my $order_amount = $order->netamount or return;
97 return $self->abschlag
98 ? $self->netamount / $order_amount
104 die 'not a setter method' if @_;
106 return ($self->amount || 0) - ($self->netamount || 0);
109 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
113 return $self->paid >= $self->amount;
116 sub _clone_orderitem_delivery_order_item_cvar {
119 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
120 $cloned->sub_module('invoice');
126 my ($class, $source, %params) = @_;
128 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
129 croak("Cannot create invoices for purchase records") unless $source->customer_id;
131 require SL::DB::Employee;
133 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
134 my (@columns, @item_columns);
136 if (ref($source) eq 'SL::DB::Order') {
137 @columns = qw(quonumber payment_id delivery_customer_id delivery_vendor_id);
138 @item_columns = qw(subtotal);
141 @columns = qw(donumber);
144 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
145 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
146 transdate => DateTime->today_local,
147 gldate => DateTime->today_local,
148 duedate => DateTime->today_local->add(days => $terms * 1),
153 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
156 if ($source->type =~ /_order$/) {
157 $args{deliverydate} = $source->reqdate;
158 $args{orddate} = $source->transdate;
160 $args{quodate} = $source->transdate;
163 my $invoice = $class->new(%args, %{ $params{attributes} || {} });
164 my $items = delete($params{items}) || $source->items_sorted;
167 my $source_item = $_;
168 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
170 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
171 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id ordnumber transdate cusordnumber unit
172 base_qty longdescription lastcost price_factor_id), @item_columns),
173 deliverydate => $source_item->reqdate,
174 fxsellprice => $source_item->sellprice,
175 custom_variables => \@custom_variables,
180 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
182 $invoice->invoiceitems(\@items);
188 my ($self, %params) = @_;
190 require SL::DB::Chart;
191 if (!$params{ar_id}) {
192 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
195 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
196 $params{ar_id} = $chart->id;
200 my %data = $self->calculate_prices_and_taxes;
202 $self->_post_create_assemblyitem_entries($data{assembly_items});
205 $self->_post_add_acctrans($data{amounts_cogs});
206 $self->_post_add_acctrans($data{amounts});
207 $self->_post_add_acctrans($data{taxes});
209 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
211 $self->_post_update_allocated($data{allocated});
214 if ($self->db->in_transaction) {
216 } elsif (!$self->db->do_transaction($worker)) {
217 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
224 sub _post_add_acctrans {
225 my ($self, $entries) = @_;
227 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
230 require SL::DB::AccTransaction;
231 require SL::DB::Chart;
232 while (my ($chart_id, $spec) = each %{ $entries }) {
233 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
234 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
237 SL::DB::AccTransaction->new(trans_id => $self->id,
238 chart_id => $chart_id,
239 amount => $spec->{amount},
240 tax_id => $spec->{tax_id},
241 taxkey => $spec->{taxkey},
242 project_id => $self->globalproject_id,
243 transdate => $self->transdate,
244 chart_link => $chart_link)->save;
248 sub _post_create_assemblyitem_entries {
249 my ($self, $assembly_entries) = @_;
251 my $items = $self->invoiceitems;
255 foreach my $item (@{ $items }) {
256 next if $item->assemblyitem;
258 push @new_items, $item;
261 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
262 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
263 description => $assembly_item->{part}->description,
264 unit => $assembly_item->{part}->unit,
265 qty => $assembly_item->{qty},
266 allocated => $assembly_item->{allocated},
269 assemblyitem => 't');
273 $self->invoiceitems(\@new_items);
276 sub _post_update_allocated {
277 my ($self, $allocated) = @_;
279 while (my ($invoice_id, $diff) = each %{ $allocated }) {
280 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
281 where => [ id => $invoice_id ]);
288 return 'ar_transaction' if !$self->invoice;
289 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
290 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
291 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
295 sub displayable_state {
298 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
313 SL::DB::Invoice: Rose model for invoices (table "ar")
319 =item C<new_from $source, %params>
321 Creates a new C<SL::DB::Invoice> instance and copies as much
322 information from C<$source> as possible. At the moment only sales
323 orders and sales quotations are supported as sources.
325 The conversion copies order items into invoice items. Dates are copied
326 as appropriate, e.g. the C<transdate> field from an order will be
327 copied into the invoice's C<orddate> field.
329 C<%params> can include the following options:
335 An optional array reference of RDBO instances for the items to use. If
336 missing then the method C<items_sorted> will be called on
337 C<$source>. This option can be used to override the sorting, to
338 exclude certain positions or to add additional ones.
340 =item C<skip_items_zero_qty>
342 If trueish then items with a quantity of 0 are skipped.
346 An optional hash reference. If it exists then it is passed to C<new>
347 allowing the caller to set certain attributes for the new delivery
352 Amounts, prices and taxes are not
353 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
354 can be used for this.
356 The object returned is not saved.
358 =item C<post %params>
360 Posts the invoice. Required parameters are:
366 The ID of the accounds receivable chart the invoices amounts are
367 posted to. If it is not set then the first chart configured for
368 accounts receivables is used.
372 This function implements several steps:
376 =item 1. It calculates all prices, amounts and taxes by calling
377 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
379 =item 2. A new and unique invoice number is created.
381 =item 3. All amounts for costs of goods sold are recorded in
384 =item 4. All amounts for parts, services and assemblies are recorded
385 in C<acc_trans> with their respective charts. This is determined by
386 the part's buchungsgruppen.
388 =item 5. The total amount is posted to the accounts receivable chart
389 and recorded in C<acc_trans>.
391 =item 6. Items in C<invoice> are updated according to their allocation
392 status (regarding for costs of goold sold). Will only be done if
393 kivitendo is not configured to use Einnahmenüberschussrechnungen.
395 =item 7. The invoice and its items are saved.
399 Returns C<$self> on success and C<undef> on failure. The whole process
400 is run inside a transaction. If it fails then nothing is saved to or
401 changed in the database. A new transaction is only started if none is
404 =item C<basic_info $field>
406 See L<SL::DB::Object::basic_info>.
412 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>