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 SL::DB::MetaSetup::Invoice;
12 use SL::DB::Manager::Invoice;
13 use SL::DB::Helper::FlattenToForm;
14 use SL::DB::Helper::LinkedRecords;
15 use SL::DB::Helper::PriceTaxCalculator;
16 use SL::DB::Helper::PriceUpdater;
17 use SL::DB::Helper::TransNumberGenerator;
18 use SL::DB::AccTransaction;
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 __PACKAGE__->meta->initialize;
48 sub items { goto &invoiceitems; }
53 return [ sort {$a->id <=> $b->id } @{ $self->items } ];
57 # For compatibility with Order, DeliveryOrder
58 croak 'not an accessor' if @_ > 1;
62 # it is assumed, that ordnumbers are unique here.
63 sub first_order_by_ordnumber {
66 my $orders = SL::DB::Manager::Order->get_all(
68 ordnumber => $self->ordnumber,
73 return first { $_->is_type('sales_order') } @{ $orders };
76 sub abschlag_percentage {
78 my $order = $self->first_order_by_ordnumber or return;
79 my $order_amount = $order->netamount or return;
80 return $self->abschlag
81 ? $self->netamount / $order_amount
87 die 'not a setter method' if @_;
89 return ($self->amount || 0) - ($self->netamount || 0);
92 __PACKAGE__->meta->make_attr_helpers(taxamount => 'numeric(15,5)');
96 return $self->paid >= $self->amount;
100 my ($class, $source, %params) = @_;
102 croak("Unsupported source object type '" . ref($source) . "'") unless ref($source) =~ m/^ SL::DB:: (?: Order | DeliveryOrder ) $/x;
103 croak("Cannot create invoices for purchase records") unless $source->customer_id;
105 my $terms = $source->can('payment_id') && $source->payment_id ? $source->payment_terms->terms_netto : 0;
107 my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber quonumber
108 department_id cp_id language_id payment_id delivery_customer_id delivery_vendor_id taxzone_id shipto_id
109 globalproject_id transaction_description currency_id)),
110 transdate => DateTime->today_local,
111 gldate => DateTime->today_local,
112 duedate => DateTime->today_local->add(days => $terms * 1),
117 employee_id => (SL::DB::Manager::Employee->current || SL::DB::Employee->new(id => $source->employee_id))->id,
120 if ($source->type =~ /_order$/) {
121 $args{deliverydate} = $source->reqdate;
122 $args{orddate} = $source->transdate;
124 $args{quodate} = $source->transdate;
127 my $invoice = $class->new(%args, %params);
130 my $source_item = $_;
131 SL::DB::InvoiceItem->new(map({ ( $_ => $source_item->$_ ) }
132 qw(parts_id description qty sellprice discount project_id
133 serialnumber pricegroup_id ordnumber transdate cusordnumber unit
134 base_qty subtotal longdescription lastcost price_factor_id)),
135 deliverydate => $source_item->reqdate,
136 fxsellprice => $source_item->sellprice,);
137 } @{ $source->items_sorted };
139 $invoice->invoiceitems(\@items);
145 my ($self, %params) = @_;
147 if (!$params{ar_id}) {
148 my $chart = SL::DB::Manager::Chart->get_all(query => [ SL::DB::Manager::Chart->link_filter('AR') ],
151 croak("No AR chart found and no parameter `ar_id' given") unless $chart;
152 $params{ar_id} = $chart->id;
156 my %data = $self->calculate_prices_and_taxes;
158 $self->_post_create_assemblyitem_entries($data{assembly_items});
159 $self->create_trans_number;
162 $self->_post_add_acctrans($data{amounts_cogs});
163 $self->_post_add_acctrans($data{amounts});
164 $self->_post_add_acctrans($data{taxes});
166 $self->_post_add_acctrans({ $params{ar_id} => $self->amount * -1 });
168 $self->_post_update_allocated($data{allocated});
171 if ($self->db->in_transaction) {
173 } elsif (!$self->db->do_transaction($worker)) {
174 $::lxdebug->message(LXDebug->WARN(), "convert_to_invoice failed: " . join("\n", (split(/\n/, $self->db->error))[0..2]));
181 sub _post_add_acctrans {
182 my ($self, $entries) = @_;
184 my $default_tax_id = SL::DB::Manager::Tax->find_by(taxkey => 0)->id;
187 while (my ($chart_id, $spec) = each %{ $entries }) {
188 $spec = { taxkey => 0, tax_id => $default_tax_id, amount => $spec } unless ref $spec;
189 $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
192 SL::DB::AccTransaction->new(trans_id => $self->id,
193 chart_id => $chart_id,
194 amount => $spec->{amount},
195 tax_id => $spec->{tax_id},
196 taxkey => $spec->{taxkey},
197 project_id => $self->globalproject_id,
198 transdate => $self->transdate,
199 chart_link => $chart_link)->save;
203 sub _post_create_assemblyitem_entries {
204 my ($self, $assembly_entries) = @_;
206 my $items = $self->invoiceitems;
210 foreach my $item (@{ $items }) {
211 next if $item->assemblyitem;
213 push @new_items, $item;
216 foreach my $assembly_item (@{ $assembly_entries->[$item_idx] || [ ] }) {
217 push @new_items, SL::DB::InvoiceItem->new(parts_id => $assembly_item->{part},
218 description => $assembly_item->{part}->description,
219 unit => $assembly_item->{part}->unit,
220 qty => $assembly_item->{qty},
221 allocated => $assembly_item->{allocated},
224 assemblyitem => 't');
228 $self->invoiceitems(\@new_items);
231 sub _post_update_allocated {
232 my ($self, $allocated) = @_;
234 while (my ($invoice_id, $diff) = each %{ $allocated }) {
235 SL::DB::Manager::InvoiceItem->update_all(set => { allocated => { sql => "allocated + $diff" } },
236 where => [ id => $invoice_id ]);
243 return 'ar_transaction' if !$self->invoice;
244 return 'credit_note' if $self->type eq 'credit_note' && $self->amount < 0 && !$self->storno;
245 return 'invoice_storno' if $self->type ne 'credit_note' && $self->amount < 0 && $self->storno;
246 return 'credit_note_storno' if $self->type eq 'credit_note' && $self->amount > 0 && $self->storno;
250 sub displayable_state {
253 return $self->closed ? $::locale->text('closed') : $::locale->text('open');
268 SL::DB::Invoice: Rose model for invoices (table "ar")
274 =item C<new_from $source>
276 Creates a new C<SL::DB::Invoice> instance and copies as much
277 information from C<$source> as possible. At the moment only sales
278 orders and sales quotations are supported as sources.
280 The conversion copies order items into invoice items. Dates are copied
281 as appropriate, e.g. the C<transdate> field from an order will be
282 copied into the invoice's C<orddate> field.
284 Amounts, prices and taxes are not
285 calculated. L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>
286 can be used for this.
288 The object returned is not saved.
290 =item C<post %params>
292 Posts the invoice. Required parameters are:
298 The ID of the accounds receivable chart the invoices amounts are
299 posted to. If it is not set then the first chart configured for
300 accounts receivables is used.
304 This function implements several steps:
308 =item 1. It calculates all prices, amounts and taxes by calling
309 L<SL::DB::Helper::PriceTaxCalculator::calculate_prices_and_taxes>.
311 =item 2. A new and unique invoice number is created.
313 =item 3. All amounts for costs of goods sold are recorded in
316 =item 4. All amounts for parts, services and assemblies are recorded
317 in C<acc_trans> with their respective charts. This is determined by
318 the part's buchungsgruppen.
320 =item 5. The total amount is posted to the accounts receivable chart
321 and recorded in C<acc_trans>.
323 =item 6. Items in C<invoice> are updated according to their allocation
324 status (regarding for costs of goold sold). Will only be done if
325 kivitendo is not configured to use Einnahmenüberschussrechnungen.
327 =item 7. The invoice and its items are saved.
331 Returns C<$self> on success and C<undef> on failure. The whole process
332 is run inside a transaction. If it fails then nothing is saved to or
333 changed in the database. A new transaction is only started if none is
336 =item C<basic_info $field>
338 See L<SL::DB::Object::basic_info>.
344 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>