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' ] }
44 class => 'SL::DB::Shipto',
45 column_map => { id => 'trans_id' },
46 query_args => [ module => 'AR' ],
49 type => 'one to many',
50 class => 'SL::DB::AccTransaction',
51 column_map => { id => 'trans_id' },
53 with_objects => [ 'chart' ],
54 sort_by => 'acc_trans_id ASC',
59 __PACKAGE__->meta->initialize;
61 __PACKAGE__->attr_html('notes');
62 __PACKAGE__->attr_sorted('items');
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; }
82 # For compatibility with Order, DeliveryOrder
83 croak 'not an accessor' if @_ > 1;
87 # it is assumed, that ordnumbers are unique here.
88 sub first_order_by_ordnumber {
91 my $orders = SL::DB::Manager::Order->get_all(
93 ordnumber => $self->ordnumber,
98 return first { $_->is_type('sales_order') } @{ $orders };
101 sub abschlag_percentage {
103 my $order = $self->first_order_by_ordnumber or return;
104 my $order_amount = $order->netamount or return;
105 return $self->abschlag
106 ? $self->netamount / $order_amount
112 die 'not a setter method' if @_;
114 return ($self->amount || 0) - ($self->netamount || 0);
117 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
121 return $self->paid >= $self->amount;
124 sub _clone_orderitem_delivery_order_item_cvar {
127 my $cloned = Rose::DB::Object::Helpers::clone_and_reset($_);
128 $cloned->sub_module('invoice');
134 my ($class, $source, %params) = @_;
136 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
137 croak("Cannot create invoices for purchase records") unless $source->customer_id;
139 require SL::DB::Employee;
141 my (@columns, @item_columns, $item_parent_id_column, $item_parent_column);
143 if (ref($source) eq 'SL::DB::Order') {
144 @columns = qw(quonumber delivery_customer_id delivery_vendor_id);
145 @item_columns = qw(subtotal);
147 $item_parent_id_column = 'trans_id';
148 $item_parent_column = 'order';
151 @columns = qw(donumber);
153 $item_parent_id_column = 'delivery_order_id';
154 $item_parent_column = 'delivery_order';
157 my $terms = $source->can('payment_id') ? $source->payment_terms : undef;
159 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
160 cp_id language_id taxzone_id shipto_id globalproject_id transaction_description currency_id delivery_term_id payment_id), @columns),
161 transdate => DateTime->today_local,
162 gldate => DateTime->today_local,
163 duedate => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local,
168 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
171 if ($source->type =~ /_order$/) {
172 $args{deliverydate} = $source->reqdate;
173 $args{orddate} = $source->transdate;
175 $args{quodate} = $source->transdate;
178 my $invoice = $class->new(%args);
179 $invoice->assign_attributes(%{ $params{attributes} }) if $params{attributes};
180 my $items = delete($params{items}) || $source->items_sorted;
184 my $source_item = $_;
185 my $source_item_id = $_->$item_parent_id_column;
186 my @custom_variables = map { _clone_orderitem_delivery_order_item_cvar($_) } @{ $source_item->custom_variables };
188 $item_parents{$source_item_id} ||= $source_item->$item_parent_column;
189 my $item_parent = $item_parents{$source_item_id};
190 my $current_invoice_item =
191 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
192 qw(parts_id description qty sellprice discount project_id serialnumber pricegroup_id transdate cusordnumber unit
193 base_qty longdescription lastcost price_factor_id active_discount_source active_price_source), @item_columns),
194 deliverydate => $source_item->reqdate,
195 fxsellprice => $source_item->sellprice,
196 custom_variables => \@custom_variables,
197 ordnumber => ref($item_parent) eq 'SL::DB::Order' ? $item_parent->ordnumber : $source_item->ordnumber,
198 donumber => ref($item_parent) eq 'SL::DB::DeliveryOrder' ? $item_parent->donumber : $source_item->can('donumber') ? $source_item->donumber : '',
201 $current_invoice_item->{"converted_from_orderitems_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::Order';
202 $current_invoice_item->{"converted_from_delivery_order_items_id"} = $_->{id} if ref($item_parent) eq 'SL::DB::DeliveryOrder';
203 $current_invoice_item;
206 @items = grep { $_->qty * 1 } @items if $params{skip_items_zero_qty};
207 @items = grep { $_->qty >=0 } @items if $params{skip_items_negative_qty};
209 $invoice->invoiceitems(\@items);
215 my ($self, %params) = @_;
217 require SL::DB::Chart;
218 if (!$params{ar_id}) {
219 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
222 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
223 $params{ar_id} = $chart->id;
227 my %data = $self->calculate_prices_and_taxes;
229 $self->_post_create_assemblyitem_entries($data{assembly_items});
232 $self->_post_add_acctrans($data{amounts_cogs});
233 $self->_post_add_acctrans($data{amounts});
234 $self->_post_add_acctrans($data{taxes});
236 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
238 $self->_post_update_allocated($data{allocated});
241 if ($self->db->in_transaction) {
243 } elsif (!$self->db->do_transaction($worker)) {
244 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
251 sub _post_add_acctrans {
252 my ($self, $entries) = @_;
254 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
257 require SL::DB::AccTransaction;
258 require SL::DB::Chart;
259 while (my ($chart_id, $spec) = each %{ $entries }) {
260 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
261 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
264 SL::DB::AccTransaction->new(trans_id => $self->id,
265 chart_id => $chart_id,
266 amount => $spec->{amount},
267 tax_id => $spec->{tax_id},
268 taxkey => $spec->{taxkey},
269 project_id => $self->globalproject_id,
270 transdate => $self->transdate,
271 chart_link => $chart_link)->save;
275 sub _post_create_assemblyitem_entries {
276 my ($self, $assembly_entries) = @_;
278 my $items = $self->invoiceitems;
282 foreach my $item (@{ $items }) {
283 next if $item->assemblyitem;
285 push @new_items, $item;
288 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
289 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
290 description => $assembly_item->{part}->description,
291 unit => $assembly_item->{part}->unit,
292 qty => $assembly_item->{qty},
293 allocated => $assembly_item->{allocated},
296 assemblyitem => 't');
300 $self->invoiceitems(\@new_items);
303 sub _post_update_allocated {
304 my ($self, $allocated) = @_;
306 while (my ($invoice_id, $diff) = each %{ $allocated }) {
307 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
308 where => [ id => $invoice_id ]);
315 return 'ar_transaction' if !$self->invoice;
316 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
317 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
318 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
322 sub displayable_state {
325 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
328 sub displayable_type {
331 return t8('AR Transaction') if $self->invoice_type eq 'ar_transaction';
332 return t8('Credit Note') if $self->invoice_type eq 'credit_note';
333 return t8('Invoice') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'invoice_storno';
334 return t8('Credit Note') . "(" . t8('Storno') . ")" if $self->invoice_type eq 'credit_note_storno';
335 return t8('Invoice');
341 return t8('AR Transaction (abbreviation)') if $self->invoice_type eq 'ar_transaction';
342 return t8('Credit note (one letter abbreviation)') if $self->invoice_type eq 'credit_note';
343 return t8('Invoice (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'invoice_storno';
344 return t8('Credit note (one letter abbreviation)') . "(" . t8('Storno (one letter abbreviation)') . ")" if $self->invoice_type eq 'credit_note_storno';
345 return t8('Invoice (one letter abbreviation)');
364 $html = SL::Presenter->get->sales_invoice($self, display => 'inline') if $self->invoice;
365 $html = SL::Presenter->get->ar_transaction($self, display => 'inline') if !$self->invoice;
378 SL::DB::Invoice: Rose model for invoices (table "ar")
384 =item C<new_from $source, %params>
386 Creates a new C<SL::DB::Invoice> instance and copies as much
387 information from C<$source> as possible. At the moment only sales
388 orders and sales quotations are supported as sources.
390 The conversion copies order items into invoice items. Dates are copied
391 as appropriate, e.g. the C<transdate> field from an order will be
392 copied into the invoice's C<orddate> field.
394 C<%params> can include the following options:
400 An optional array reference of RDBO instances for the items to use. If
401 missing then the method C<items_sorted> will be called on
402 C<$source>. This option can be used to override the sorting, to
403 exclude certain positions or to add additional ones.
405 =item C<skip_items_negative_qty>
407 If trueish then items with a negative quantity are skipped. Items with
408 a quantity of 0 are not affected by this option.
410 =item C<skip_items_zero_qty>
412 If trueish then items with a quantity of 0 are skipped.
416 An optional hash reference. If it exists then it is passed to C<new>
417 allowing the caller to set certain attributes for the new delivery
422 Amounts, prices and taxes are not
423 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
424 can be used for this.
426 The object returned is not saved.
428 =item C<post %params>
430 Posts the invoice. Required parameters are:
436 The ID of the accounds receivable chart the invoices amounts are
437 posted to. If it is not set then the first chart configured for
438 accounts receivables is used.
442 This function implements several steps:
446 =item 1. It calculates all prices, amounts and taxes by calling
447 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
449 =item 2. A new and unique invoice number is created.
451 =item 3. All amounts for costs of goods sold are recorded in
454 =item 4. All amounts for parts, services and assemblies are recorded
455 in C<acc_trans> with their respective charts. This is determined by
456 the part's buchungsgruppen.
458 =item 5. The total amount is posted to the accounts receivable chart
459 and recorded in C<acc_trans>.
461 =item 6. Items in C<invoice> are updated according to their allocation
462 status (regarding for costs of goold sold). Will only be done if
463 kivitendo is not configured to use Einnahmenüberschussrechnungen.
465 =item 7. The invoice and its items are saved.
469 Returns C<$self> on success and C<undef> on failure. The whole process
470 is run inside a transaction. If it fails then nothing is saved to or
471 changed in the database. A new transaction is only started if none is
474 =item C<basic_info $field>
476 See L<SL::DB::Object::basic_info>.
482 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>