]> wagnertech.de Git - mfinanz.git/blob - SL/DB/PurchaseInvoice.pm
kivitendo 3.9.2-0.2
[mfinanz.git] / SL / DB / PurchaseInvoice.pm
1 package SL::DB::PurchaseInvoice;
2
3 use strict;
4
5 use Carp;
6 use Data::Dumper;
7 use List::Util qw(sum);
8
9 use SL::DB::MetaSetup::PurchaseInvoice;
10 use SL::DB::Manager::PurchaseInvoice;
11 use SL::DB::Helper::AttrHTML;
12 use SL::DB::Helper::AttrSorted;
13 use SL::DB::Helper::LinkedRecords;
14 use SL::DB::Helper::Payment qw(:ALL);
15 use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
16 use SL::DB::Helper::SalesPurchaseInvoice;
17 use SL::DB::Helper::ZUGFeRD qw(:IMPORT);
18 use SL::Locale::String qw(t8);
19 use Rose::DB::Object::Helpers qw(has_loaded_related forget_related as_tree strip);
20
21 # The calculator hasn't been adjusted for purchase invoices yet.
22 # use SL::DB::Helper::PriceTaxCalculator;
23
24 __PACKAGE__->meta->add_relationship(
25   invoiceitems   => {
26     type         => 'one to many',
27     class        => 'SL::DB::InvoiceItem',
28     column_map   => { id => 'trans_id' },
29     manager_args => { with_objects => [ 'part' ] }
30   },
31   sepa_export_items => {
32     type            => 'one to many',
33     class           => 'SL::DB::SepaExportItem',
34     column_map      => { id => 'ap_id' },
35     manager_args    => { with_objects => [ 'sepa_export' ] }
36   },
37   sepa_exports      => {
38     type            => 'many to many',
39     map_class       => 'SL::DB::SepaExportItem',
40     map_from        => 'ap',
41     map_to          => 'sepa_export',
42   },
43   custom_shipto     => {
44     type            => 'one to one',
45     class           => 'SL::DB::Shipto',
46     column_map      => { id => 'trans_id' },
47     query_args      => [ module => 'AP' ],
48   },
49   transactions   => {
50     type         => 'one to many',
51     class        => 'SL::DB::AccTransaction',
52     column_map   => { id => 'trans_id' },
53     manager_args => { with_objects => [ 'chart' ],
54                       sort_by      => 'acc_trans_id ASC' }
55   },
56 );
57
58 __PACKAGE__->meta->initialize;
59
60 __PACKAGE__->attr_html('notes');
61 __PACKAGE__->attr_sorted('items');
62
63 __PACKAGE__->after_save('_after_save_link_records');
64
65 # hooks
66
67 sub _after_save_link_records {
68   my ($self) = @_;
69
70   my @allowed_record_sources = qw(SL::DB::Reclamation SL::DB::Order);
71   my @allowed_item_sources = qw(SL::DB::ReclamationItem SL::DB::OrderItem);
72
73   SL::DB::Helper::RecordLink::link_records(
74     $self,
75     \@allowed_record_sources,
76     \@allowed_item_sources,
77     close_source_quotations => 1,
78   );
79 }
80
81 # methods
82
83 sub items { goto &invoiceitems; }
84 sub add_items { goto &add_invoiceitems; }
85 sub record_number { goto &invnumber; }
86 sub record_type { goto &invoice_type; }
87
88 sub is_sales {
89   # For compatibility with Order, DeliveryOrder
90   croak 'not an accessor' if @_ > 1;
91   return 0;
92 }
93
94 sub date {
95   goto &transdate;
96 }
97
98 sub reqdate {
99   goto &duedate;
100 }
101
102 sub customervendor {
103   goto &vendor;
104 }
105
106 sub abbreviation {
107   my $self = shift;
108
109   return t8('AP Transaction (abbreviation)') if !$self->invoice && !$self->storno;
110   return t8('AP Transaction (abbreviation)') . '(' . t8('Storno (one letter abbreviation)') . ')' if !$self->invoice && $self->storno;
111   return t8('Invoice (one letter abbreviation)'). '(' . t8('Storno (one letter abbreviation)') . ')' if $self->storno;
112   return t8('Invoice (one letter abbreviation)');
113
114 }
115
116 sub oneline_summary {
117   my $self = shift;
118
119   return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->invnumber, $self->vendor->name,
120                                       $::form->format_amount(\%::myconfig, $self->amount,2), $self->transdate->to_kivitendo);
121 }
122
123 sub link {
124   my ($self) = @_;
125
126   my $html;
127   $html   = $self->presenter->purchase_invoice(display => 'inline') if $self->invoice;
128   $html   = $self->presenter->ap_transaction(display => 'inline') if !$self->invoice;
129
130   return $html;
131 }
132
133 sub invoice_type {
134   my ($self) = @_;
135
136   return 'purchase_credit_note'  if  $self->amount < 0;
137   return 'ap_transaction'        if !$self->invoice;
138   return 'purchase_invoice';
139 }
140 sub is_credit_note {
141   my ($self) = @_;
142
143   return $self->invoice_type eq 'purchase_credit_note' ? 1 : undef;
144 }
145
146 sub displayable_type {
147   my ($self) = @_;
148
149   return t8('AP Transaction')    if $self->invoice_type eq 'ap_transaction';
150   return t8('Purchase Invoice');
151 }
152
153 sub displayable_name {
154   join ' ', grep $_, map $_[0]->$_, qw(displayable_type record_number);
155 }
156
157 sub convert_to_reclamation {
158   my ($self, %params) = @_;
159   $params{destination_type} = $self->is_sales ? 'sales_reclamation'
160                                               : 'purchase_reclamation';
161
162   require SL::DB::Reclamation;
163   my $reclamation = SL::DB::Reclamation->new_from($self, %params);
164
165   return $reclamation;
166 }
167
168 sub create_from_zugferd_data {
169   my ($class, $data) = @_;
170
171   my $ap_invoice = $class->new();
172
173   $ap_invoice->import_zugferd_data($data);
174 }
175
176 sub create_ap_row {
177   my ($self, %params) = @_;
178   # needs chart as param
179   # to be called after adding all AP_amount rows
180
181   # only allow this method for ap invoices (Kreditorenbuchung)
182   die if $self->invoice and not $self->vendor_id;
183
184   return 0 unless scalar @{$self->transactions} > 0;
185
186   my $acc_trans = [];
187   my $chart = $params{chart} || SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ap_chart_id);
188   die "illegal chart in create_ap_row" unless $chart;
189
190   die "receivables chart must have link 'AP'" . Dumper($chart) unless $chart->link eq 'AP';
191
192   # hardcoded entry for no tax, tax_id and taxkey should be 0
193   my $tax = SL::DB::Manager::Tax->find_by(id => 0, taxkey => 0) || die "Can't find tax with id 0 and taxkey 0";
194
195   my $sign = $self->vendor_id ? 1 : -1;
196   my $acc = SL::DB::AccTransaction->new(
197     amount     => $self->amount * $sign,
198     chart_id   => $params{chart}->id,
199     chart_link => $params{chart}->link,
200     transdate  => $self->transdate,
201     taxkey     => $tax->taxkey,
202     tax_id     => $tax->id,
203   );
204   $self->add_transactions( $acc );
205   push( @$acc_trans, $acc );
206   return $acc_trans;
207 }
208
209 sub add_ap_amount_row {
210   my ($self, %params ) = @_;
211
212   # only allow this method for ap invoices (Kreditorenbuchung)
213   die "not an ap invoice" if $self->invoice and not $self->vendor_id;
214
215   die "add_ap_amount_row needs a chart object as chart param" unless $params{chart} && $params{chart}->isa('SL::DB::Chart');
216   die unless $params{chart}->link =~ /AP_amount/;
217
218   my $acc_trans = [];
219
220   my $roundplaces = 2;
221   my ($netamount,$taxamount);
222
223   $netamount = $params{amount} * 1;
224   my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id}) || die "Can't find tax with id " . $params{tax_id};
225
226   if ( $tax and $tax->rate != 0 ) {
227     ($netamount, $taxamount) = Form->calculate_tax($params{amount}, $tax->rate, $self->taxincluded, $roundplaces);
228   }
229
230   return unless $netamount; # netamount mustn't be zero
231
232   my $sign = $self->vendor_id ? -1 : 1;
233   my $acc = SL::DB::AccTransaction->new(
234     amount     => $netamount * $sign,
235     chart_id   => $params{chart}->id,
236     chart_link => $params{chart}->link,
237     transdate  => $self->transdate,
238     gldate     => $self->gldate,
239     taxkey     => $tax->taxkey,
240     tax_id     => $tax->id,
241     project_id => $params{project_id},
242   );
243
244   $self->add_transactions( $acc );
245   push( @$acc_trans, $acc );
246
247   if ( $taxamount ) {
248      my $acc = SL::DB::AccTransaction->new(
249        amount     => $taxamount * $sign,
250        chart_id   => $tax->chart_id,
251        chart_link => $tax->chart->link,
252        transdate  => $self->transdate,
253        gldate     => $self->gldate,
254        taxkey     => $tax->taxkey,
255        tax_id     => $tax->id,
256        project_id => $params{project_id},
257      );
258      $self->add_transactions( $acc );
259      push( @$acc_trans, $acc );
260   }
261   return $acc_trans;
262 }
263
264 sub validate_acc_trans {
265   my ($self, %params) = @_;
266   # should be able to check unsaved invoice objects with several acc_trans lines
267
268   die "validate_acc_trans can't check invoice object with empty transactions" unless $self->transactions;
269
270   my @transactions = @{$self->transactions};
271   # die "invoice has no acc_transactions" unless scalar @transactions > 0;
272
273   return 0 unless scalar @transactions > 0;
274   return 0 unless $self->has_loaded_related('transactions');
275
276   $::lxdebug->message(LXDebug->DEBUG1(), sprintf("starting validatation of purchase invoice %s with trans_id %s and taxincluded %s\n", $self->invnumber // '', $self->id // '', $self->taxincluded // ''));
277   foreach my $acc ( @transactions ) {
278     $::lxdebug->message(LXDebug->DEBUG1(), sprintf("chart: %s  amount: %s   tax_id: %s  link: %s\n", $acc->chart->accno, $acc->amount, $acc->tax_id, $acc->chart->link));
279   }
280
281   my $acc_trans_sum = sum map { $_->amount } @transactions;
282
283   unless ( $::form->round_amount($acc_trans_sum, 10) == 0 ) {
284     my $string = "sum of acc_transactions isn't 0: $acc_trans_sum\n";
285
286     foreach my $trans ( @transactions ) {
287       $string .= sprintf("  %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount);
288     }
289     $::lxdebug->message(LXDebug->DEBUG1(), $string);
290     return 0;
291   }
292
293   # only use the first AP entry, so it also works for paid invoices
294   my @ap_transactions = map { $_->amount } grep { $_->chart_link eq 'AP' } @transactions;
295   my $ap_sum = $ap_transactions[0];
296   # my $ap_sum = sum map { $_->amount } grep { $_->chart_link eq 'AP' } @transactions;
297
298   my $sign = $self->vendor_id ? 1 : -1;
299
300   unless ( $::form->round_amount($ap_sum * $sign, 2) == $::form->round_amount($self->amount, 2) ) {
301
302     $::lxdebug->message(LXDebug->DEBUG1(), sprintf("debug: (ap_sum) %s = %s (amount)\n",  $::form->round_amount($ap_sum * $sign, 2) , $::form->round_amount($self->amount, 2) ) );
303     foreach my $trans ( @transactions ) {
304       $::lxdebug->message(LXDebug->DEBUG1(), sprintf("  %s %s %s %s\n", $trans->chart->accno, $trans->taxkey, $trans->amount, $trans->chart->link));
305     }
306
307     die sprintf("sum of ap (%s) isn't equal to invoice amount (%s)", $::form->round_amount($ap_sum * $sign, 2), $::form->round_amount($self->amount, 2));
308   }
309
310   return 1;
311 }
312
313 sub recalculate_amounts {
314   my ($self, %params) = @_;
315   # calculate and set amount and netamount from acc_trans objects
316
317   croak ("Can only recalculate amounts for ap transactions") if $self->invoice;
318
319   return undef unless $self->has_loaded_related('transactions');
320
321   my ($netamount, $taxamount);
322
323   my @transactions = @{$self->transactions};
324
325   foreach my $acc ( @transactions ) {
326     $netamount += $acc->amount if $acc->chart->link =~ /AP_amount/;
327     $taxamount += $acc->amount if $acc->chart->link =~ /AP_tax/;
328   }
329
330   my $sign = $self->vendor_id ? -1 : 1;
331   $self->amount   (($netamount + $taxamount) * $sign);
332   $self->netamount(($netamount)              * $sign);
333 }
334
335 sub mark_as_paid {
336   my ($self) = @_;
337
338   $self->update_attributes(paid => $self->amount);
339 }
340
341 sub effective_tax_point {
342   my ($self) = @_;
343
344   return $self->tax_point || $self->deliverydate || $self->transdate;
345 }
346
347 sub netamount_base_currency {
348   my ($self) = @_;
349
350   return $self->netamount; # already matches base currency
351 }
352
353 1;
354
355
356 __END__
357
358 =pod
359
360 =encoding UTF-8
361
362 =head1 NAME
363
364 SL::DB::PurchaseInvoice: Rose model for purchase invoices (table "ap")
365
366 =head1 FUNCTIONS
367
368 =over 4
369
370 =item C<create_ap_row>
371
372 =item C<add_ap_amount_row>
373
374 =item C<validate_acc_trans>
375
376 =item C<recalculate_amounts>
377
378 These functions are similar to the ones in the C<SL::DB::Invoice> module. See
379 there for more information.
380
381 =item C<mark_as_paid>
382
383 Marks the invoice as paid by setting its C<paid> member to the value of C<amount>.
384
385 =back
386
387 =head1 AUTHOR
388
389 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
390
391 =cut