Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision f49e9deb

Von Sven Schöling vor 9 Monaten hinzugefügt

  • ID f49e9deb77039f7cac10d23ab93ebb9cd66b8870
  • Vorgänger b0c61725

PoC: Invoice Controller

Das hier ist der Versuch mit dem aktuellen TypeData und Model::Record
Unterbau einen Invoice Controller zu bauen, vor allem zu
Dokumentationszwecken.

Dieser Branch ist NICHT zum mergen gedacht.

Erkenntnisse, was alles anders ist oder angefasst werde muss:

Im Controller:
- $self->order > $self>invoice
- Mit Tamino und Bernd besprochen, sollte eher generisch $self->record
sein
- ValidityToken::SCOPE_ORDER_SAVE > SCOPE_SALES_INVOICE_POST
- Da gab es wohl schon einmal Diskussionen ob die überhaupt scopes
haben müssen. Ansonsten, nach TypeData verschieben.
- init_type Fehlermeldung: "not a valid type foe order"
- type_data Proxy muss mit der richtigen Klasse SL::DB::Invoice erstellt
werden.
- Items müssen SL::DB::InvoiceItem statt SL::DB::OrderItem sein
- $::form
>{order} > $::form>{invoice}
- auch hier - vereinheitlichen auf $::form->{record}?
- $::form->{orderitems} > $::form>{invoiceitems}
- dito.
- setup_custom_shipto module OE -> AR
- javascripte müssen umgebogen werden und evtl frontend checks neu
gebaut werden:
- kivi.Order.check_cv
- kivi.Order.check_duplicate_parts
- kivi.Order.check_valid_reqdate
- kivi.Order.check_transport_cost_article_presence
- kivi.Order.check_cusordnumber_presence
- kivi.Order.check_has_final_invoice - unnötig
- kivi.Order.check_invoice_advance_payment - unnötig

Features:
- close_quotations - gibt es in Invoice nicht
- periodic_invoices - gibt es in Invoice nicht
- basket_from_from - (sic!) gibt es in Invoice nicht
- shipped_qty - Invoice macht im Moment nichts mit Lager im Frontend
- transport_cost_reminder - gibt es in Invoice nicht
- phone_notes - gibt es in Invoice nicht
- subversion / final_version - gibt es in Invoice nicht als Zieltypen
im workflow
- not_order_locked - Waren nicht nicht mehr eingekauft werden dürfen
- ja/nein?

In SL::DB::Invoice und TypeDate
- Invoice kennt bereits ein type, was aber nicht das gleiche ist wie
das record_type. Das muss in der Datenbank gefixt werden.
- duedate/reqdate default belegung fuktioniert so nicht, ist von
payment_terms abhängig
- gldate belegung funktioniert so nicht
- parts_classification

Dazu ware in is.pl sehr viele Flags in Form, die da eigentlich nicht
hingehören:
- locked
- readonly
- storno
- storno_id
- postal_invoice
- is_gldate_ready (gldate today)
- payment_balanced (oldpaidtotal paidtotal)

- type_data->rights kann im Moment keine object rights wie über Projekte
- und die parts_classification_query sind da noch kaputt

Unterschiede anzeigen:

SL/Controller/Invoice.pm
1
package SL::Controller::Invoice;
2

  
3
use strict;
4
use parent qw(SL::Controller::Base);
5

  
6
use SL::DB::Invoice;
7
use SL::Helper::Flash qw(flash flash_later);
8
use SL::HTML::Util;
9
use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
10
use SL::Locale::String qw(t8);
11
use SL::SessionFile::Random;
12
use SL::PriceSource;
13
use SL::File;
14
use SL::YAML;
15
use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
16
use SL::DB::Helper::TypeDataProxy;
17
use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
18
use SL::Model::Record;
19
use SL::DB::Invoice::TypeData qw(:types);
20
use SL::DB::Order::TypeData qw(:types);
21
use SL::DB::DeliveryOrder::TypeData qw(:types);
22
use SL::DB::Reclamation::TypeData qw(:types);
23

  
24
use SL::Helper::CreatePDF qw(:all);
25
use SL::Helper::PrintOptions;
26
use SL::Helper::ShippedQty;
27
use SL::Helper::UserPreferences::DisplayPreferences;
28
use SL::Helper::UserPreferences::PositionsScrollbar;
29
use SL::Helper::UserPreferences::UpdatePositions;
30

  
31
use SL::Controller::Helper::GetModels;
32

  
33
use List::Util qw(first sum0);
34
use List::UtilsBy qw(sort_by uniq_by);
35
use List::MoreUtils qw(uniq any none pairwise first_index);
36
use File::Spec;
37
use Sort::Naturally;
38

  
39
use Rose::Object::MakeMethods::Generic
40
(
41
 scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
42
 'scalar --get_set_init' => [ qw(invoice valid_types type cv p all_price_factors
43
                              search_cvpartnumber show_update_button
44
                              part_picker_classification_ids
45
                              type_data) ],
46
);
47

  
48

  
49
# safety
50
__PACKAGE__->run_before('check_auth');
51
__PACKAGE__->run_before('check_auth_for_edit',
52
                        except => [ qw(edit price_popup load_second_rows) ]);
53

  
54
#
55
# actions
56
#
57

  
58
# add a newinvoice
59
sub action_add {
60
  my ($self) = @_;
61

  
62
  $self->invoice(SL::Model::Record->update_after_new($self->invoice));
63

  
64
  $self->invoice->gldate($self->invoice->payment_terms ? $self->invoice->payment_terms->calc_date(reference_date => $self->invoice->transdate) : $self->invoice->transdate);
65

  
66
  $self->pre_render();
67

  
68
  if (!$::form->{form_validity_token}) {
69
    $::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_SALES_INVOICE_POST())->token;
70
  }
71

  
72
  $self->render(
73
    'invoice/form',
74
    title => $self->type_data->text('add'),
75
    %{$self->{template_args}}
76
  );
77
}
78

  
79

  
80

  
81
#
82
# helpers
83
#
84

  
85
sub init_valid_types {
86
  $_[0]->type_data->valid_types;
87
}
88

  
89
sub init_type {
90
  my ($self) = @_;
91

  
92
  my $type = $self->invoice->record_type;
93
  if (none { $type eq $_ } @{$self->valid_types}) {
94
    die "Not a valid type for invoice";
95
  }
96

  
97
  $self->type($type);
98
}
99

  
100
sub init_cv {
101
  my ($self) = @_;
102

  
103
  return $self->type_data->properties('customervendor');
104
}
105

  
106
sub init_search_cvpartnumber {
107
  my ($self) = @_;
108

  
109
  my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
110
  my $search_cvpartnumber;
111
  $search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
112
  $search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel()        if $self->cv eq 'vendor';
113

  
114
  return $search_cvpartnumber;
115
}
116

  
117
sub init_show_update_button {
118
  my ($self) = @_;
119

  
120
  !!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
121
}
122

  
123
sub init_p {
124
  SL::Presenter->get;
125
}
126

  
127
sub init_invoice {
128
  $_[0]->make_invoice;
129
}
130

  
131
sub init_all_price_factors {
132
  SL::DB::Manager::PriceFactor->get_all;
133
}
134

  
135
sub init_part_picker_classification_ids {
136
  my ($self)    = @_;
137

  
138
  return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
139
    where => $self->type_data->part_classification_query()) } ];
140
}
141

  
142
sub init_type_data {
143
  my ($self) = @_;
144
  SL::DB::Helper::TypeDataProxy->new('SL::DB::Invoice', $self->invoice->record_type);
145
}
146

  
147
sub check_auth {
148
  my ($self) = @_;
149
  $::auth->assert($self->type_data->rights('view'));
150
}
151

  
152
sub check_auth_for_edit {
153
  my ($self) = @_;
154
  $::auth->assert($self->type_data->rights('edit'));
155
}
156

  
157

  
158
#
159
# internal
160
#
161

  
162

  
163
sub pre_render {
164
  my ($self) = @_;
165

  
166
  $self->{all_taxzones}               = SL::DB::Manager::TaxZone->get_all_sorted();
167
  $self->{all_currencies}             = SL::DB::Manager::Currency->get_all_sorted();
168
  $self->{all_departments}            = SL::DB::Manager::Department->get_all_sorted();
169
  $self->{all_languages}              = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->invoice->language_id ] ] );
170
  $self->{all_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->employee_id,
171
                                                                                              deleted => 0 ] ],
172
                                                                           sort_by => 'name');
173
  $self->{all_salesmen}               = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->salesman_id,
174
                                                                                              deleted => 0 ] ],
175
                                                                           sort_by => 'name');
176
  $self->{all_payment_terms}          = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->invoice->payment_id,
177
                                                                                                        obsolete => 0 ] ]);
178
  $self->{all_delivery_terms}         = SL::DB::Manager::DeliveryTerm->get_valid($self->invoice->delivery_term_id);
179
  $self->{current_employee_id}        = SL::DB::Manager::Employee->current->id;
180
  $self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
181

  
182
  my $print_form = Form->new('');
183
  $print_form->{type}        = $self->type;
184
  $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
185
  $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
186
    form => $print_form,
187
    options => {dialog_name_prefix => 'print_options.',
188
                show_headers       => 1,
189
                no_queue           => 1,
190
                no_postscript      => 1,
191
                no_opendocument    => 0,
192
                no_html            => 0},
193
  );
194

  
195
  foreach my $item (@{$self->invoice->items}) {
196
    my $price_source = SL::PriceSource->new(record_item => $item, record => $self->invoice);
197
    $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
198
    $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
199
  }
200

  
201

  
202
  # ?! TODO: does invoices need stock info?
203
#   if (any { $self->type eq $_ } (INVOICE_TYPE(), INVOICE_FOR_ADVANCE_PAYMENT_TYPE(), INVOICE_FOR_ADVANCE_PAYMENT_STORNO_TYPE(), FINAL_INVOICE_TYPE(), INVOICE_STORNO_TYPE(), CREDIT_NOTE_TYPE(), CREDIT_NOTE_STORNO_TYPE())) {
204
#     # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
205
#     # Do not use write_to_objects to prevent order->delivered to be set, because this should be
206
#     # the value from db, which can be set manually or is set when linked delivery orders are saved.
207
#     SL::Helper::ShippedQty->new->calculate($self->record)->write_to(\@{$self->order->items});
208
#   }
209

  
210
  if ($self->invoice->number && $::instance_conf->get_webdav) {
211
    my $webdav = SL::Webdav->new(
212
      type     => $self->type,
213
      number   => $self->invoice->number,
214
    );
215
    my @all_objects = $webdav->get_all_objects;
216
    @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
217
                                                    type => t8('File'),
218
                                                    link => File::Spec->catfile($_->full_filedescriptor),
219
                                                } } @all_objects;
220
  }
221

  
222
#   if (   (any { $self->type eq $_ } (SALES_QUOTATION_TYPE(), SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()))
223
#       && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
224
#     $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
225
#   }
226
  $self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
227

  
228
  $self->get_item_cvpartnumber($_) for @{$self->invoice->items_sorted};
229

  
230
#   $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
231

  
232
  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Invoice kivi.File
233
                                                         calculate_qty follow_up show_history);
234
  $self->setup_edit_action_bar;
235
}
236

  
237

  
238
sub setup_edit_action_bar {
239
  my ($self, %params) = @_;
240

  
241
  my $change_never            = $::instance_conf->get_is_changeable == 0;
242
  my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && $self->invoice->gldate->clone->truncate(to => 'day') != DateTime->today;
243
  my $payments_balanced       = 0; #($::form->{oldtotalpaid} == 0); # TODO: don't rely on form
244
  my $has_storno              = 0; # $self->invoice->linked_record('storno'); # TODO: linked record?
245
  my $may_edit_create         = $::auth->assert($self->type_data->rights('edit'), 'may fail');
246
  my $factur_x_enabled        = $self->invoice->customer && $self->invoice->customer->create_zugferd_invoices_for_this_customer;
247
  my ($is_linked_bank_transaction, $warn_unlinked_delivery_order);
248
    if ($::form->{id}
249
        && SL::DB::Default->get->payments_changeable != 0
250
        && SL::DB::Manager::BankTransactionAccTrans->find_by(ar_id => $::form->{id})) {
251

  
252
      $is_linked_bank_transaction = 1;
253
    }
254
  if ($::instance_conf->get_warn_no_delivery_order_for_invoice && !$self->invoice->id) {
255
    $warn_unlinked_delivery_order = 1 unless $::form->{convert_from_do_ids};
256
  }
257

  
258
  my $has_further_invoice_for_advance_payment;
259
  if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
260
    my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
261
    $has_further_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
262
  }
263

  
264
  my $has_final_invoice;
265
  if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
266
    my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
267
    $has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->invoice_type} @$lr;
268
  }
269

  
270
  my $is_invoice_for_advance_payment_from_order;
271
  if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
272
    my $lr = $self->invoice->linked_records(direction => 'from', from => ['Order']);
273
    $is_invoice_for_advance_payment_from_order = scalar @$lr >= 1;
274
  }
275

  
276
  my $locked = 0; # TODO: get from... somewhere
277

  
278
  # add readonly state in tmpl_vars
279
#   $tmpl_var->{readonly} = !$may_edit_create                     ? 1
280
#                     : $form->{locked}                           ? 1
281
#                     : $form->{storno}                           ? 1
282
#                     : ($form->{id} && $change_never)            ? 1
283
#                     : ($form->{id} && $change_on_same_day_only) ? 1
284
#                     : $is_linked_bank_transaction               ? 1
285
#                     : 0;
286

  
287

  
288

  
289
  for my $bar ($::request->layout->get('actionbar')) {
290
    $bar->add(
291
      action => [
292
        t8('Update'),
293
        submit    => [ '#form', { action => "update" } ],
294
        disabled  => !$may_edit_create ? t8('You must not change this invoice.')
295
                   : $locked           ? t8('The billing period has already been locked.')
296
                   :                     undef,
297
        id        => 'update_button',
298
        accesskey => 'enter',
299
      ],
300

  
301
      combobox => [
302
        action => [
303
          t8('Post'),
304
          submit   => [ '#form', { action => "post" } ],
305
          checks   => [ 'kivi.validate_form' ],
306
          confirm  => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
307
          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
308
                    : $locked                                   ? t8('The billing period has already been locked.')
309
                    : $self->invoice->storno                    ? t8('A canceled invoice cannot be posted.')
310
                    : $self->invoice->id && $change_never       ? t8('Changing invoices has been disabled in the configuration.')
311
                    : $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
312
                    : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
313
                    :                                             undef,
314
        ],
315
        action => [
316
          t8('Post and Close'),
317
          submit   => [ '#form', { action => "post_and_close" } ],
318
          checks   => [ 'kivi.validate_form' ],
319
          confirm  => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
320
          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
321
                    : $locked                                   ? t8('The billing period has already been locked.')
322
                    : $self->invoice->storno                    ? t8('A canceled invoice cannot be posted.')
323
                    : $self->invoice->id && $change_never       ? t8('Changing invoices has been disabled in the configuration.')
324
                    : $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
325
                    : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
326
                    :                                             undef,
327
        ],
328
        action => [
329
          t8('Post Payment'),
330
          submit   => [ '#form', { action => "post_payment" } ],
331
          checks   => [ 'kivi.validate_form' ],
332
          disabled => !$may_edit_create           ? t8('You must not change this invoice.')
333
                    : !$self->invoice->id         ? t8('This invoice has not been posted yet.')
334
                    : $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
335
                    :                               undef,
336
            only_if  => $self->invoice->record_type ne "invoice_for_advance_payment",
337
        ],
338
        action => [ t8('Mark as paid'),
339
          submit   => [ '#form', { action => "mark_as_paid" } ],
340
          confirm  => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
341
          disabled => !$may_edit_create ? t8('You must not change this invoice.')
342
                    : !$self->invoice->id ? t8('This invoice has not been posted yet.')
343
                    :                     undef,
344
          only_if  => ($::instance_conf->get_is_show_mark_as_paid && $self->invoice->record_type ne "invoice_for_advance_payment")
345
                   || $self->invoice->record_type eq 'final_invoice',
346
        ],
347
      ], # end of combobox "Post"
348

  
349
      combobox => [
350
        action => [ t8('Storno'),
351
          submit   => [ '#form', { action => "storno" } ],
352
          confirm  => t8('Do you really want to cancel this invoice?'),
353
          checks   => [ 'kivi.validate_form' ],
354
          disabled => !$may_edit_create   ? t8('You must not change this invoice.')
355
                    : $locked             ? t8('The billing period has already been locked.')
356
                    : !$self->invoice->id ? t8('This invoice has not been posted yet.')
357
                    : $self->invoice->storno ? t8('Cannot storno storno invoice!')
358
                    : !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
359
                    : undef,
360
        ],
361
        action => [ t8('Delete'),
362
          submit   => [ '#form', { action => "delete" } ],
363
          confirm  => t8('Do you really want to delete this object?'),
364
          checks   => [ 'kivi.validate_form' ],
365
          disabled => !$may_edit_create        ? t8('You must not change this invoice.')
366
                    : !$self->invoice->id      ? t8('This invoice has not been posted yet.')
367
                    : $locked                  ? t8('The billing period has already been locked.')
368
                    : $change_never            ? t8('Changing invoices has been disabled in the configuration.')
369
                    : $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
370
                    : $has_storno              ? t8('Can only delete the "Storno zu" part of the cancellation pair.')
371
                    :                            undef,
372
        ],
373
      ], # end of combobox "Storno"
374

  
375
      'separator',
376

  
377
      combobox => [
378
        action => [ t8('Workflow') ],
379
        action => [
380
          t8('Use As New'),
381
          submit   => [ '#form', { action => "use_as_new" } ],
382
          checks   => [ 'kivi.validate_form' ],
383
          disabled => !$may_edit_create ? t8('You must not change this invoice.')
384
                    : !$self->invoice->id ? t8('This invoice has not been posted yet.')
385
                    :                     undef,
386
        ],
387
        action => [
388
          t8('Further Invoice for Advance Payment'),
389
          submit   => [ '#form', { action => "further_invoice_for_advance_payment" } ],
390
          checks   => [ 'kivi.validate_form' ],
391
          disabled => !$may_edit_create                          ? t8('You must not change this invoice.')
392
                    : !$self->invoice->id                        ? t8('This invoice has not been posted yet.')
393
                    : $has_further_invoice_for_advance_payment   ? t8('This invoice has already a further invoice for advanced payment.')
394
                    : $has_final_invoice                         ? t8('This invoice has already a final invoice.')
395
                    : $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
396
                    :                                              undef,
397
          only_if  => $self->invoice->record_type eq "invoice_for_advance_payment",
398
        ],
399
        action => [
400
          t8('Final Invoice'),
401
          submit   => [ '#form', { action => "final_invoice" } ],
402
          checks   => [ 'kivi.validate_form' ],
403
          disabled => !$may_edit_create                          ? t8('You must not change this invoice.')
404
                    : !$self->invoice->id                        ? t8('This invoice has not been posted yet.')
405
                    : $has_further_invoice_for_advance_payment   ? t8('This invoice has a further invoice for advanced payment.')
406
                    : $has_final_invoice                         ? t8('This invoice has already a final invoice.')
407
                    : $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
408
                    :                                              undef,
409
          only_if  => $self->invoice->is_type("invoice_for_advance_payment"),
410
        ],
411
        action => [
412
          t8('Credit Note'),
413
          submit   => [ '#form', { action => "credit_note" } ],
414
          checks   => [ 'kivi.validate_form' ],
415
          disabled => !$may_edit_create              ? t8('You must not change this invoice.')
416
                    : $self->invoice->is_type("credit_note") ? t8('Credit notes cannot be converted into other credit notes.')
417
                    : !$self->invoice->id                   ? t8('This invoice has not been posted yet.')
418
                    : $self>invocie->storno                ? t8('A canceled invoice cannot be used. Please undo the cancellation first.')
419
                    :                                  undef,
420
        ],
421
        action => [
422
          t8('Sales Order'),
423
          submit   => [ '#form', { action => "order" } ],
424
          checks   => [ 'kivi.validate_form' ],
425
          disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
426
        ],
427
        action => [
428
          t8('Reclamation'),
429
          submit   => ['#form', { action => "sales_reclamation" }], # can't call Reclamation directly
430
          disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
431
          only_if   => ($self->invoice->is_type('invoice') && !$::form->{storno}),
432
        ],
433
      ], # end of combobox "Workflow"
434

  
435
      combobox => [
436
        action => [ t8('Export') ],
437
        action => [
438
          ($self->invoice->id ? t8('Print') : t8('Preview')),
439
          call     => [ 'kivi.SalesPurchase.show_print_dialog', $self->invoice->id ? 'print' : 'preview' ],
440
          checks   => [ 'kivi.validate_form' ],
441
          disabled => !$may_edit_create               ? t8('You must not print this invoice.')
442
                    : !$self->invoice->id && $locked ? t8('The billing period has already been locked.')
443
                    :                                   undef,
444
        ],
445
        action => [ t8('Print and Post'),
446
          call     => [ 'kivi.SalesPurchase.show_print_dialog', 'print_and_post' ],
447
          checks   => [ 'kivi.validate_form' ],
448
          confirm  => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
449
          disabled => !$may_edit_create                         ? t8('You must not change this invoice.')
450
                    : $locked                                   ? t8('The billing period has already been locked.')
451
                    : $self->invoice->storno                    ? t8('A canceled invoice cannot be posted.')
452
                    : ($self->invoice->id && $change_never)            ? t8('Changing invoices has been disabled in the configuration.')
453
                    : ($self->invoice->id && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
454
                    : $is_linked_bank_transaction               ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
455
                    :                                             undef,
456
        ],
457
        action => [ t8('E Mail'),
458
          call     => [ 'kivi.SalesPurchase.show_email_dialog' ],
459
          checks   => [ 'kivi.validate_form' ],
460
          disabled => !$may_edit_create       ? t8('You must not print this invoice.')
461
                    : !$self->invoice->id     ? t8('This invoice has not been posted yet.')
462
                    : $self->invoice->customer->postal_invoice ? t8('This customer wants a postal invoices.')
463
                    :                     undef,
464
        ],
465
        action => [ t8('Factur-X/ZUGFeRD'),
466
          submit   => [ '#form', { action => "download_factur_x_xml" } ],
467
          checks   => [ 'kivi.validate_form' ],
468
          disabled => !$may_edit_create   ? t8('You must not print this invoice.')
469
                    : !$self->invoice->id ? t8('This invoice has not been posted yet.')
470
                    : !$factur_x_enabled  ? t8('Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.')
471
                    :                      undef,
472
        ],
473
      ], # end of combobox "Export"
474

  
475
      combobox => [
476
        action => [ t8('more') ],
477
        action => [
478
          t8('History'),
479
          call     => [ 'set_history_window', $self->invoice->id * 1, 'glid' ],
480
          disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
481
        ],
482
        action => [
483
          t8('Follow-Up'),
484
          call     => [ 'follow_up_window' ],
485
          disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
486
        ],
487
        action => [
488
          t8('Drafts'),
489
          call     => [ 'kivi.Draft.popup', 'is', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
490
          disabled => !$may_edit_create   ? t8('You must not change this invoice.')
491
                    : $self->invoice->id  ? t8('This invoice has already been posted.')
492
                    : $locked             ? t8('The billing period has already been locked.')
493
                    :                     undef,
494
        ],
495
      ], # end of combobox "more"
496
    );
497
  }
498
}
499

  
500
# load or create a new order object
501
#
502
# And assign changes from the form to this object.
503
# If the order is loaded from db, check if items are deleted in the form,
504
# remove them form the object and collect them for removing from db on saving.
505
# Then create/update items from form (via make_item) and add them.
506
sub make_invoice {
507
  my ($self) = @_;
508

  
509
  # add_items adds items to an order with no items for saving, but they cannot
510
  # be retrieved via items until the order is saved. Adding empty items to new
511
  # order here solves this problem.
512
  my $invoice;
513
  $invoice   = SL::DB::Invoice->new(id => $::form->{id})->load(with => [ 'invoiceitems', 'invoiceitems.part' ]) if $::form->{id};
514
  $invoice ||= SL::DB::Invoice->new(invoiceitems  => [],
515
                                record_type => $::form->{type},
516
                                currency_id => $::instance_conf->get_currency_id(),);
517

  
518
  my $cv_id_method = $invoice->type_data->properties('customervendor'). '_id';
519
  if (!$::form->{id} && $::form->{$cv_id_method}) {
520
    $invoice->$cv_id_method($::form->{$cv_id_method});
521
    $invoice = SL::Model::Record->update_after_customer_vendor_change($invoice);
522
  }
523

  
524
  my $form_invoiceitems                = delete $::form->{invoice}->{invoiceitems};
525

  
526
  $invoice->assign_attributes(%{$::form->{invoice}});
527

  
528
  $self->setup_custom_shipto_from_form($invoice, $::form);
529

  
530
  # remove deleted items
531
  $self->item_ids_to_delete([]);
532
  foreach my $idx (reverse 0..$#{$invoice->invoiceitems}) {
533
    my $item = $invoice->invoiceitems->[$idx];
534
    if (none { $item->id == $_->{id} } @{$form_invoiceitems}) {
535
      splice @{$invoice->invoiceitems}, $idx, 1;
536
      push @{$self->item_ids_to_delete}, $item->id;
537
    }
538
  }
539

  
540
  my @items;
541
  my $pos = 1;
542
  foreach my $form_attr (@{$form_invoiceitems}) {
543
    my $item = make_item($invoice, $form_attr);
544
    $item->position($pos);
545
    push @items, $item;
546
    $pos++;
547
  }
548
  $invoice->add_items(grep {!$_->id} @items);
549

  
550
  return $invoice;
551
}
552

  
553
# create or update items from form
554
#
555
# Make item objects from form values. For items already existing read from db.
556
# Create a new item else. And assign attributes.
557
sub make_item {
558
  my ($record, $attr) = @_;
559

  
560
  my $item;
561
  $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
562

  
563
  my $is_new = !$item;
564

  
565
  # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
566
  # they cannot be retrieved via custom_variables until the order/orderitem is
567
  # saved. Adding empty custom_variables to new orderitem here solves this problem.
568
  $item ||= SL::DB::InvoiceItem->new(custom_variables => []);
569

  
570
  $item->assign_attributes(%$attr);
571

  
572
  if ($is_new) {
573
    my $texts = get_part_texts($item->part, $record->language_id);
574
    $item->longdescription($texts->{longdescription})              if !defined $attr->{longdescription};
575
    $item->project_id($record->globalproject_id)                   if !defined $attr->{project_id};
576
    $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
577
  }
578

  
579
  return $item;
580
}
581

  
582
# setup custom shipto from form
583
#
584
# The dialog returns form variables starting with 'shipto' and cvars starting
585
# with 'shiptocvar_'.
586
# Mark it to be deleted if a shipto from master data is selected
587
# (i.e. order has a shipto).
588
# Else, update or create a new custom shipto. If the fields are empty, it
589
# will not be saved on save.
590
sub setup_custom_shipto_from_form {
591
  my ($self, $record, $form) = @_;
592

  
593
  if ($record->shipto) {
594
    $self->is_custom_shipto_to_delete(1);
595
  } else {
596
    my $custom_shipto = $record->custom_shipto || $record->custom_shipto(SL::DB::Shipto->new(module => 'AR', custom_variables => []));
597

  
598
    my $shipto_cvars  = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
599
    my $shipto_attrs  = {map {                                  $_   => delete $form->{$_}} grep { m{^shipto}      } keys %$form};
600

  
601
    $custom_shipto->assign_attributes(%$shipto_attrs);
602
    $custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
603
  }
604
}
605

  
606
1;
SL/DB/Invoice.pm
19 19
use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
20 20
use SL::DB::Helper::SalesPurchaseInvoice;
21 21
use SL::DB::Helper::TransNumberGenerator;
22
use SL::DB::Helper::TypeDataProxy;
22 23
use SL::DB::Helper::ZUGFeRD qw(:CREATE);
23 24
use SL::Locale::String qw(t8);
24 25

  
......
79 80
__PACKAGE__->before_save('_before_save_set_invnumber');
80 81
__PACKAGE__->after_save('_after_save_link_records');
81 82

  
83
use Rose::Object::MakeMethods::Generic (
84
  'scalar --get_set_init' => [ qw(record_type) ],
85
);
86

  
82 87
# hooks
83 88

  
84 89
sub _before_save_set_invnumber {
......
108 113
sub items { goto &invoiceitems; }
109 114
sub add_items { goto &add_invoiceitems; }
110 115
sub record_number { goto &invnumber; };
111
sub record_type { goto &invoice_type; };
116
#sub record_type { goto &invoice_type; };
112 117

  
113 118
sub is_sales {
114 119
  # For compatibility with Order, DeliveryOrder
......
576 581
  }
577 582
}
578 583

  
584
sub init_record_type {
585
  goto &invoice_type;
586
}
587

  
579 588
sub invoice_type {
580 589
  my ($self) = @_;
581 590

  
......
645 654
  goto &customer;
646 655
}
647 656

  
657
sub is_type {
658
  return shift->record_type eq shift;
659
}
660

  
661
sub number {
662
  my $self = shift;
663

  
664
  my $nr_key = $self->type_data->properties('nr_key');
665
  return $self->$nr_key(@_);
666
}
667

  
648 668
sub link {
649 669
  my ($self) = @_;
650 670

  
......
673 693
  return $self->netamount; # already matches base currency
674 694
}
675 695

  
696
sub type_data {
697
  SL::DB::Helper::TypeDataProxy->new(ref $_[0], $_[0]->record_type);
698
}
699

  
676 700
1;
677 701

  
678 702
__END__
SL/DB/Invoice/TypeData.pm
46 46
      worflow_needed => 0,
47 47
    },
48 48
    defaults => {
49
      # TODO
49
      # aka: duedate
50
      reqdate => sub {
51
        #$invoice->payment_terms ? $invoice->payment_terms->calc_date(reference_date => $::form->{invdate})->to_kivitendo : $invoice->transdate;
52
        DateTime->today
53
      },
50 54
    },
51
    part_classification_query => [ "used_for_sales" => 1 ],
55
    part_classification_query => [ "used_for_sale" => 1 ],
52 56
    rights => {
53
      # TODO
57
      edit => "invoice_edit",
58
      view => "invoice_edit | sales_invoice_view",
54 59
    },
55 60
    features => {
56 61
      price_tax   => 1,
......
84 89
    defaults => {
85 90
      # TODO
86 91
    },
87
    part_classification_query => [ "used_for_sales" => 1 ],
92
    part_classification_query => [ "used_for_sale" => 1 ],
88 93
    rights => {
89 94
      # TODO
90 95
    },
......
120 125
    defaults => {
121 126
      # TODO
122 127
    },
123
    part_classification_query => [ "used_for_sales" => 1 ],
128
    part_classification_query => [ "used_for_sale" => 1 ],
124 129
    rights => {
125 130
      # TODO
126 131
    },
......
156 161
    defaults => {
157 162
      # TODO
158 163
    },
159
    part_classification_query => [ "used_for_sales" => 1 ],
164
    part_classification_query => [ "used_for_sale" => 1 ],
160 165
    rights => {
161 166
      # TODO
162 167
    },
......
192 197
    defaults => {
193 198
      # TODO
194 199
    },
195
    part_classification_query => [ "used_for_sales" => 1 ],
200
    part_classification_query => [ "used_for_sale" => 1 ],
196 201
    rights => {
197 202
      # TODO
198 203
    },
......
228 233
    defaults => {
229 234
      # TODO
230 235
    },
231
    part_classification_query => [ "used_for_sales" => 1 ],
236
    part_classification_query => [ "used_for_sale" => 1 ],
232 237
    rights => {
233 238
      # TODO
234 239
    },
......
264 269
    defaults => {
265 270
      # TODO
266 271
    },
267
    part_classification_query => [ "used_for_sales" => 1 ],
272
    part_classification_query => [ "used_for_sale" => 1 ],
268 273
    rights => {
269 274
      # TODO
270 275
    },
SL/Model/Record.pm
27 27

  
28 28
  $new_record->transdate(DateTime->now_local());
29 29

  
30
  my $default_reqdate = $new_record->type_data->defaults('reqdate');
31
  $new_record->reqdate($default_reqdate);
30
  if ($new_record->can('reqdate')) {
31
    my $default_reqdate = $new_record->type_data->defaults('reqdate');
32
    $new_record->reqdate($default_reqdate);
33
  }
32 34

  
33 35
  return $new_record;
34 36
}
templates/webpages/invoice/form.html
1
[%- USE T8 %]
2
[%- USE LxERP %]
3
[%- USE L %]
4
[%- USE HTML %]
5

  
6
[% IF FORM.workflow_email_journal_id %]
7
  [% SET FORM.title = LxERP.t8("Email Journal Workflow") _ " - " _ FORM.title %]
8
[% END %]
9
<h1>[% FORM.title %] <span id='nr_in_title'>[%- SELF.invoice.number -%]</span></h1>
10

  
11
<div id="print_options" style="display:none">
12
  <form method="post" action="controller.pl" id="print_options_form">
13
    [% SELF.print_options %]
14
    <br>
15
    [% L.button_tag('kivi.Order.print()', LxERP.t8('Print')) %]
16
    <a href="#" onclick="$('#print_options').dialog('close');">[% LxERP.t8("Cancel") %]</a>
17
  </form>
18
</div>
19

  
20
<div id="shipto_dialog" class="hidden"></div>
21

  
22
<form method="post" action="controller.pl" id="invoice_form"
23
      data-transport-cost-reminder-article-id="[% HTML.escape(transport_cost_reminder_article.id) %]"
24
      data-transport-cost-reminder-article-description="[% HTML.escape(transport_cost_reminder_article.displayable_name) %]">
25
  [% L.hidden_tag('callback',             FORM.callback) %]
26
  [% L.hidden_tag('type',                 FORM.type) %]
27
  [% L.hidden_tag('id',                   SELF.invoice.id) %]
28
  [% L.hidden_tag('converted_from_record_type_ref', SELF.invoice.converted_from_record_type_ref) %]
29
  [% L.hidden_tag('converted_from_record_id',       SELF.invoice.converted_from_record_id) %]
30
  [% L.hidden_tag('email_journal_id',               FORM.email_journal_id) %]
31
  [% L.hidden_tag('email_attachment_id',            FORM.email_attachment_id) %]
32
  [% L.hidden_tag('workflow_email_journal_id',      FORM.workflow_email_journal_id) %]
33
  [% L.hidden_tag('workflow_email_attachment_id',   FORM.workflow_email_attachment_id) %]
34
  [% L.hidden_tag('workflow_email_callback',        FORM.workflow_email_callback) %]
35

  
36
  [% IF !SELF.invoice.id %]
37
  [%   L.hidden_tag('form_validity_token', FORM.form_validity_token) %]
38
  [% END %]
39

  
40
  [%- INCLUDE 'common/flash.html' %]
41

  
42
  <div class="tabwidget" id="invoice_tabs">
43
    <ul>
44
      <li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
45
[% IF FORM.email_attachment_id || FORM.workflow_email_attachment_id %]
46
      <li><a href="controller.pl?action=EmailJournal/attachment_preview&attachment_id=[% HTML.url(FORM.email_attachment_id || FORM.workflow_email_attachment_id) %]">[% 'Email Attachment Preview' | $T8 %]</a></li>
47
[% END %]
48
[%- IF INSTANCE_CONF.get_webdav %]
49
      <li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
50
[%- END %]
51
[%- IF SELF.invoice.id AND INSTANCE_CONF.get_doc_storage %]
52
      <li><a href="controller.pl?action=File/list&file_type=document&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.invoice.id) %]">[% 'Documents' | $T8 %]</a></li>
53
      <li><a href="controller.pl?action=File/list&file_type=attachment&object_type=[% HTML.escape(FORM.type) %]&object_id=[% HTML.url(SELF.invoice.id) %]">[% 'Attachments' | $T8 %]</a></li>
54
[%- END %]
55
[%- IF SELF.invoice.id %]
56
      <li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Invoice&object_id=[% HTML.url(SELF.invoice.id) %]">[% 'Linked Records' | $T8 %]</a></li>
57
[%- END %]
58
[% IF SELF.invoice.id %]
59
      <li><a href="#ui-tabs-phone-notes">[% 'Phone Notes' | $T8 %]<span id="num_phone_notes">[%- num_phone_notes ? ' (' _ num_phone_notes _ ')' : '' -%]</span></a></li>
60
[% END %]
61
    </ul>
62

  
63
    [% PROCESS "invoice/tabs/basic_data.html" %]
64
    [% PROCESS 'webdav/_list.html' %]
65
    <div id="ui-tabs-1">
66
      [%- LxERP.t8("Loading...") %]
67
    </div>
68
[% IF SELF.invoice.id %]
69
    <div id="ui-tabs-phone-notes">
70
      [% PROCESS "invoice/tabs/phone_notes.html" %]
71
    </div>
72
[% END %]
73
    <div id="shipto_inputs" class="hidden">
74
      [%- PROCESS 'common/_ship_to_dialog.html'
75
        vc_obj=SELF.invoice.customervendor
76
        cs_obj=SELF.invoice.custom_shipto
77
        cvars=SELF.invoice.custom_shipto.cvars_by_config
78
        id_selector='#invoice_shipto_id' %]
79
    </div>
80

  
81
  </div>
82
</form>
templates/webpages/invoice/tabs/_business_info_row.html
1
[%- USE T8 %][%- USE HTML %]
2

  
3
<tr id='business_info_row' [%- IF !SELF.order.customervendor.business_id %]style='display:none'[%- END %]>
4
  <th align="right">[%- IF SELF.cv == 'customer' -%]
5
                      [%- 'Customer type' | $T8 -%]
6
                    [%- ELSE -%]
7
                      [%- 'Vendor type' | $T8 -%]
8
                    [%- END -%]</th>
9
  <td>[% HTML.escape(SELF.order.customervendor.business.description) %]; [% 'Trade Discount' | $T8 %] [% SELF.order.customervendor.business.discount_as_percent %] %</td>
10
</tr>
templates/webpages/invoice/tabs/_item_input.html
1
[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %][%- USE P %]
2

  
3
<div>
4
  <table id="input_row_table_id">
5
    <thead>
6
      <tr class="listheading">
7
        <th class="listheading" nowrap >[%- '+'            | $T8 %] </th>
8
        <th class="listheading" nowrap >[%- 'position'     | $T8 %] </th>
9
        <th class="listheading" nowrap >[%- 'Part'         | $T8 %] </th>
10
        <th class="listheading" nowrap >[%- 'Description'  | $T8 %] </th>
11
        <th class="listheading" nowrap width="5" >[%- 'Qty'          | $T8 %] </th>
12
        <th class="listheading" nowrap width="15">[%- 'Price'        | $T8 %] </th>
13
        <th class="listheading" nowrap width="5" >[%- 'Discount'     | $T8 %] </th>
14
        <th></th>
15
      </tr>
16
    </thead>
17
    <tbody>
18
      <tr valign="top" class="listrow">
19
        <td class="tooltipster-html" title="[%- 'Create a new part' | $T8 -%]">
20
          [% SET type_options = [[ 'part', LxERP.t8('Part') ], [ 'assembly', LxERP.t8('Assembly') ], [ 'service', LxERP.t8('Service') ] ] %]
21
          [%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
22
            [%- type_options.push([ 'assortment', LxERP.t8('Assortment')]) %]
23
          [%- END %]
24
          [% L.select_tag('add_item.create_part_type', type_options) %]
25
          [% L.button_tag('kivi.Order.create_part()', LxERP.t8('+')) %]
26
        </td>
27
        <td>[% L.input_tag('add_item.position', '', size = 5, class="add_item_input numeric") %]</td>
28
        <td>
29
          [%- SET PARAM_KEY = SELF.cv == "customer" ? 'with_customer_partnumber' : 'with_makemodel' -%]
30
          [%- SET PARAM_VAL = SELF.search_cvpartnumber -%]
31
          [% P.part.picker('add_item.parts_id', SELF.created_part, style='width: 300px', class="add_item_input",
32
                            multiple_pos_input=1,
33
                            action={set_multi_items='kivi.Order.add_multi_items'},
34
                            classification_id=SELF.part_picker_classification_ids.as_list.join(','),
35
                            $PARAM_KEY=PARAM_VAL) %]</td>
36
        <td>[% L.input_tag('add_item.description', SELF.created_part.description, class="add_item_input") %]</td>
37
        <td>
38
          [% L.input_tag('add_item.qty_as_number', '', placeholder="1", size = 5, class="add_item_input numeric") %]
39
          [% L.hidden_tag('add_item.unit', SELF.created_part.unit, class="add_item_input") %]
40
        </td>
41
        [%- SET price = '' %]
42
        [%- IF SELF.created_part %]
43
          [%- SET price = LxERP.format_amount(((SELF.type == 'sales_quotation' || SELF.type == 'sales_order_intake' || SELF.type == 'sales_order') ? SELF.created_part.sellprice : SELF.created_part.lastcost), -2) -%]
44
        [%- END %]
45
        <td>[% L.input_tag('add_item.sellprice_as_number', price, size = 10, class="add_item_input numeric tooltipster-html") %]</td>
46
        <td>[% L.input_tag('add_item.discount_as_percent', '', size = 5, class="add_item_input numeric tooltipster-html") %]</td>
47
        <td>[% L.button_tag('kivi.Order.add_item()', LxERP.t8('Add part')) %]</td>
48
      </tr>
49
    </tbody>
50
  </table>
51
 </div>
templates/webpages/invoice/tabs/_price_sources_dialog.html
1
[%- USE T8 %]
2
[%- USE HTML %]
3
[%- USE L %]
4
[%- USE LxERP %]
5
[% SET best_price = price_source.best_price %]
6
[% SET best_discount = price_source.best_discount %]
7
[% SET price_editable = 0 %]
8
[% IF (FORM.type == "sales_order" || FORM.type == "sales_order_intake" || FORM.type == "sales_quotation") %]
9
  [% SET price_editable = AUTH.assert('sales_edit_prices', 1) %]
10
[% END %]
11
[% IF (FORM.type == "purchase_order" || FORM.type == "purchase_order_confirmation" || FORM.type == "request_quotation" || FORM.type == "purchase_quotation_intake") %]
12
  [% SET price_editable = AUTH.assert('purchase_edit_prices', 1) %]
13
[% END %]
14
[% SET exfactor = price_source.record.exchangerate ? 1 / price_source.record.exchangerate : 1 %]
15
[% SET exnoshow = price_source.record.currency_id==INSTANCE_CONF.get_currency_id %]
16
[% SET places   = exnoshow ? -2 : 5 %]
17
  <h2>[% 'Prices' | $T8 %]</h2>
18

  
19
  <table>
20
   <tr class='listheading'>
21
    <th></th>
22
    <th>[% 'Price Source' | $T8 %]</th>
23
    <th>[% 'Price' | $T8 %]</th>
24
    <th [%- IF exnoshow -%]style='display:none'[%- END %]>
25
      [% 'Price' | $T8 -%]/[%- price_source.record.currency.name %]
26
    </th>
27
    <th>[% 'Best Price' | $T8 %]</th>
28
    <th>[% 'Details' | $T8 %]</th>
29
   </tr>
30
   <tr class='listrow'>
31
[%- IF price_source.record_item.active_price_source %]
32
    <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
33
[%- ELSE %]
34
    <td><b>[% 'Selected' | $T8 %]</b></td>
35
[%- END %]
36
    <td>[% 'None (PriceSource)' | $T8 %]</td>
37
    <td>-</td>
38
    <td [%- IF exnoshow -%]style='display:none'[%- END %]>-</td>
39
    <td></td>
40
    <td></td>
41
   </tr>
42
   [%- FOREACH price IN price_source.available_prices %]
43
    <tr class='listrow'>
44
[%- IF price_source.record_item.active_price_source != price.source %]
45
     <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ LxERP.format_amount(price.price * exfactor, places) _ '\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
46
[%- ELSIF price_source.record_item.sellprice * 1 != price.price * 1 %]
47
     <td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ LxERP.format_amount(price.price * exfactor, places) _ '\', ' _ price_editable _ ')', LxERP.t8('Update Price')) %]</td>
48
[%- ELSE %]
49
    <td><b>[% 'Selected' | $T8 %]</b></td>
50
[% END %]
51
     <td>[% price.source_description | html %]</td>
52
     <td>[% price.price_as_number %]</td>
53
     <td [%- IF exnoshow -%]style='display:none'[%- END %]>
54
       [% LxERP.format_amount(price.price * exfactor, places) %]
55
     </td>
56
[% IF price.source == best_price.source %]
57
     <td align='center'>&#x2022;</td>
58
[% ELSE %]
59
     <td></td>
60
[% END %]
61
     <td>[% price.description | html %]</td>
62
    </tr>
63
   [%- END %]
64
  </table>
65

  
66
  <h2>[% 'Discounts' | $T8 %]</h2>
67

  
68
  <table>
69
   <tr class='listheading'>
70
    <th></th>
71
    <th>[% 'Price Source' | $T8 %]</th>
72
    <th>[% 'Discount' | $T8 %]</th>
73
    <th>[% 'Best Discount' | $T8 %]</th>
74
    <th>[% 'Details' | $T8 %]</th>
75
   </tr>
76
   <tr class='listrow'>
77
[%- IF price_source.record_item.active_discount_source %]
78
    <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource Discount)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
79
[%- ELSE %]
80
    <td><b>[% 'Selected' | $T8 %]</b></td>
81
[%- END %]
82
    <td>[% 'None (PriceSource Discount)' | $T8 %]</td>
83
    <td>-</td>
84
    <td></td>
85
    <td></td>
86
   </tr>
87
   [%- FOREACH price IN price_source.available_discounts %]
88
    <tr class='listrow'>
89
[%- IF price_source.record_item.active_discount_source != price.source %]
90
     <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ price.discount_as_percent _ '\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
91
[%- ELSIF price_source.record_item.discount * 1 != price.discount * 1 %]
92
     <td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'' _ price.source _ '\', \'' _ price.source_description _ '\', \'' _ price.discount_as_percent  _ '\', ' _ price_editable _ ')', LxERP.t8('Update Discount')) %]</td>
93
[%- ELSE %]
94
    <td><b>[% 'Selected' | $T8 %]</b></td>
95
[% END %]
96
     <td>[% price.source_description | html %]</td>
97
     <td>[% price.discount_as_percent %] %</td>
98
[% IF price.source == best_discount.source %]
99
     <td align='center'>&#x2022;</td>
100
[% ELSE %]
101
     <td></td>
102
[% END %]
103
     <td>[% price.description | html %]</td>
104
    </tr>
105
   [%- END %]
106
  </table>
templates/webpages/invoice/tabs/_purchase_delivery_order_item_selection.html
1
[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
2
<div>
3
  [% L.button_tag("kivi.Order.convert_to_purchase_delivery_order_item_selection()", LxERP.t8("Create delivery order")) %]
4
</div>
5

  
6
<div>
7
  <table>
8
    <thead>
9
      <tr class="listheading">
10
        <th>[% L.checkbox_tag("item_position_selection_checkall", checkall="input.item_position_selection_checkall", checked="checked") %]</th>
11
        <th>[% LxERP.t8("Position") %]</th>
12
        <th>[% LxERP.t8("Partnumber") %]</th>
13
        <th>[% LxERP.t8("Vendor Part Number") %]</th>
14
        <th>[% LxERP.t8("Description") %]</th>
15
        <th>[% LxERP.t8("Qty") %]</th>
16
        <th>[% LxERP.t8("Unit") %]</th>
17
      </tr>
18
    </thead>
19

  
20
    <tbody id="item_position_selection" class="row_entry listrow">
21
      [% FOREACH item = ITEMS %]
22
        <tr class="listrow">
23
          [% SET pos = loop.count - 1 %]
24
          <td>[% L.checkbox_tag("selected_item_positions[+]", class="item_position_selection_checkall", value=loop.count - 1, checked="checked") %]</td>
25
          <td align="right">[% loop.count %]</td>
26
          <td>[% HTML.escape(item.partnumber) %]</td>
27
          <td>[% HTML.escape(item.vendor_partnumber) %]</td>
28
          <td>[% HTML.escape(item.description) %]</td>
29
          <td align="right">[% HTML.escape(item.qty_as_number) %]</td>
30
          <td>[% HTML.escape(item.unit) %]</td>
31
        </tr>
32
      [% END %]
33
    </tbody>
34
  </table>
35
</div>
templates/webpages/invoice/tabs/_row.html
1
[%- USE T8 %]
2
[%- USE HTML %]
3
[%- USE LxERP %]
4
[%- USE L %]
5
[%- USE P %]
6

  
7
<tbody class="row_entry listrow" data-position="[%- HTML.escape(ITEM.position) -%]"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
8
  <tr>
9
    <td align="center">
10
      [%- IF MYCONFIG.show_form_details %]
11
        [% L.img_tag(src="image/collapse.svg",
12
                     alt=LxERP.t8('Hide details'), title=LxERP.t8('Hide details'), class="expand") %]
13
      [%- ELSE %]
14
        [% L.img_tag(src="image/expand.svg",
15
                     alt=LxERP.t8('Show details'), title=LxERP.t8('Show details'), class="expand") %]
16
      [%- END %]
17
      [% L.hidden_tag("orderitem_ids[+]", ID) %]
18
      [% L.hidden_tag("converted_from_record_item_type_ref[+]", ITEM.converted_from_record_item_type_ref) %]
19
      [% L.hidden_tag("converted_from_record_item_id[+]",       ITEM.converted_from_record_item_id) %]
20
      [% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %]
21
      [% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %]
22
      [% L.hidden_tag("purchase_basket_item_ids[+]", ITEM.purchase_basket_item_ids) %]
23
    </td>
24
    <td>
25
      <div name="position" class="numeric">
26
        [% HTML.escape(ITEM.position) %]
27
      </div>
28
    </td>
29
    <td align="center">
30
      <img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
31
    </td>
32
    <td align="center">
33
      [%- L.button_tag("kivi.Order.delete_order_item_row(this)",
34
                       LxERP.t8("X"),
35
                       confirm=LxERP.t8("Are you sure?")) %]
36
    </td>
37
    [%- IF SELF.show_update_button -%]
38
    <td align="center">
39
      [%- L.img_tag(src="image/rotate_cw.svg",
40
                    alt=LxERP.t8('Update from master data'),
41
                    title= LxERP.t8('Update from master data'),
42
                    onclick="if (!confirm('" _ LxERP.t8("Are you sure to update this position from master data?") _ "')) return false; kivi.Order.update_row_from_master_data(this);",
43
                    id='update_from_master') %]
44
    </td>
45
    [%- END -%]
46
    <td>
47
      <div name="partnumber">
48
        [%- P.link_tag(SELF.url_for(controller='Part', action='edit', 'part.id'=ITEM.part.id), ITEM.part.partnumber, target="_blank", title=LxERP.t8('Open in new window')) -%]
49
      </div>
50
    </td>
51
    [%- IF SELF.search_cvpartnumber -%]
52
    <td>
53
      <div name="cvpartnumber">[% HTML.escape(ITEM.cvpartnumber) %]</div>
54
    </td>
55
    [%- END -%]
56
    <td>
57
      <div name="partclassification">[% ITEM.part.presenter.typeclass_abbreviation %]</div>
58
    </td>
59
    <td>
60
      [% L.areainput_tag("order.orderitems[].description",
61
                     ITEM.description,
62
                     size='40',
63
                     style='width: 300px') %]
64
      [%- L.hidden_tag("order.orderitems[].longdescription", ITEM.longdescription) %]
65
      [%- L.button_tag("kivi.Order.show_longdescription_dialog(this)", LxERP.t8("L")) %]
66
    </td>
67
    [%- IF (SELF.type == "sales_order_intake" || SELF.type == "sales_order" || SELF.type == "purchase_order" || SELF.type == "purchase_order_confirmation") -%]
68
    <td nowrap>
69
      [%- L.div_tag(LxERP.format_amount(ITEM.shipped_qty, 2, 0) _ ' ' _ ITEM.unit, name="shipped_qty", class="numeric") %]
70
    </td>
71
    [%- END -%]
72
    <td nowrap>
73
      [%- L.input_tag("order.orderitems[].qty_as_number",
74
                      ITEM.qty_as_number,
75
                      size = 5,
76
                      class="recalc reformat_number numeric") %]
77
      [%- IF ITEM.part.formel -%]
78
        [%- L.button_tag("kivi.Order.show_calculate_qty_dialog(this)", LxERP.t8("*/")) %]
79
        [%- L.hidden_tag("formula[+]", ITEM.part.formel) -%]
80
      [%- END -%]
81
    </td>
82
    <td>
83
      [%- L.select_tag("order.orderitems[].price_factor_id",
84
                       SELF.all_price_factors,
85
                       default = ITEM.price_factor_id,
86
                       title_key = 'description',
87
                       with_empty = 1,
88
                       class="recalc") %]
89
    </td>
90
    <td nowrap>
91
      [%- L.select_tag("order.orderitems[].unit",
92
                      ITEM.part.available_units,
93
                      default = ITEM.unit,
94
                      title_key = 'name',
95
                      value_key = 'name',
96
                      class = 'unitselect') %]
97
    </td>
98
    <td>
99
      [%- L.button_tag("kivi.Order.price_chooser_item_row(this)",
100
                       ITEM.active_price_source.source_description _ ' | ' _ ITEM.active_discount_source.source_description,
101
                       name = "price_chooser_button") %]
102
    </td>
103
    [% SET RIGHT_TO_EDIT_PRICES = 0 %]
104
    [% IF (SELF.type == "sales_order_intake" || SELF.type == "sales_order" || SELF.type == "sales_quotation") %]
105
      [% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('sales_edit_prices', 1) %]
106
    [% END %]
107
    [% IF (SELF.type == "purchase_order" || SELF.type == "purchase_order_confirmation" || SELF.type == "request_quotation" || SELF.type == "purchase_quotation_intake") %]
108
      [% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('purchase_edit_prices', 1) %]
109
    [% END %]
110
    <td>
111
      [%- L.hidden_tag("order.orderitems[].active_price_source", ITEM.active_price_source.source) %]
112
      [%- SET EDIT_PRICE = (RIGHT_TO_EDIT_PRICES && ITEM.active_price_source.source == '') %]
113
      <div name="editable_price" [%- IF !EDIT_PRICE %]style="display:none"[%- END %] class="numeric">
114
        [%- L.input_tag("order.orderitems[].sellprice_as_number",
115
                        ITEM.sellprice_as_number,
116
                        size = 10,
117
                        disabled=(EDIT_PRICE? '' : 1),
118
                        class="recalc reformat_number numeric") %]
119
      </div>
120
      <div name="not_editable_price" [%- IF EDIT_PRICE %]style="display:none"[%- END %]>
121
        [%- L.div_tag(ITEM.sellprice_as_number, name="sellprice_text", class="numeric") %]
122
        [%- L.hidden_tag("order.orderitems[].sellprice_as_number",
123
                         ITEM.sellprice_as_number,
124
                         disabled=(EDIT_PRICE? 1 : '')) %]
125
      </div>
126
    </td>
127
    <td>
128
      [%- L.hidden_tag("order.orderitems[].active_discount_source", ITEM.active_discount_source.source) %]
129
      [%- SET EDIT_DISCOUNT = (RIGHT_TO_EDIT_PRICES && ITEM.active_discount_source.source == '') %]
130
      <div name="editable_discount" [%- IF !EDIT_DISCOUNT %]style="display:none"[%- END %] class="numeric">
131
        [%- L.input_tag("order.orderitems[].discount_as_percent",
132
                        ITEM.discount_as_percent,
133
                        size = 5,
134
                        disabled=(EDIT_DISCOUNT? '' : 1),
135
                        class="recalc reformat_number numeric") %]
136
      </div>
137
      <div name="not_editable_discount" [%- IF EDIT_DISCOUNT %]style="display:none"[%- END %]>
138
        [%- L.div_tag(ITEM.discount_as_percent, name="discount_text", class="numeric") %]
139
        [%- L.hidden_tag("order.orderitems[].discount_as_percent",
140
                         ITEM.discount_as_percent,
141
                         disabled=(EDIT_DISCOUNT? 1 : '')) %]
142
      </div>
143
    </td>
144
    <td align="right">
145
      [%- L.div_tag(LxERP.format_amount(ITEM.linetotal, 2, 0), name="linetotal") %]
146
    </td>
147

  
148
  </tr>
149

  
150
  <tr [%- IF !MYCONFIG.show_form_details -%]style="display:none"[%- END -%]>
151
    <td colspan="100%">
152
      [%- IF MYCONFIG.show_form_details || ITEM.render_second_row %]
153
        <div name="second_row" data-loaded="1">
154
          [%- PROCESS order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
155
        </div>
156
      [%- ELSE %]
157
        <div name="second_row" id="second_row_[% ID %]">
158
          [%- LxERP.t8("Loading...") %]
159
        </div>
160
      [%- END %]
161
    </td>
162
  </tr>
163

  
164
</tbody>
templates/webpages/invoice/tabs/_second_row.html
1
[%- USE T8 %]
2
[%- USE HTML %]
3
[%- USE LxERP %]
4
[%- USE L %]
5
[%- USE P %]
6

  
7
<table>
8
  <tr><td colspan="100%">
9
    [%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "purchase_order") %]
10
      <b>[%- 'Serial No.' | $T8 %]</b>&nbsp;
11
      [%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15 "data-validate"="trimmed_whitespaces") %]&nbsp;
12
    [%- END %]
13
    <b>[%- 'Project' | $T8 %]</b>&nbsp;
14
    [% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %]&nbsp;
15
    [%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "purchase_order") %]
16
      <b>[%- 'Reqdate' | $T8 %]</b>&nbsp;
17
      [% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %]&nbsp;
18
    [%- END %]
19
    <b>[%- 'Subtotal' | $T8 %]</b>&nbsp;
20
    [% L.yes_no_tag("order.orderitems[].subtotal", ITEM.subtotal) %]&nbsp;
21
    [%- IF TYPE == "sales_order" %]
22
      <b>[%- 'Recurring billing' | $T8 %]</b>&nbsp;
23
      [% L.select_tag("order.orderitems[].recurring_billing_mode", [[ 'always', LxERP.t8('always') ], [ 'once',   LxERP.t8('once')   ], [ 'never',  LxERP.t8('never')  ]], default=ITEM.recurring_billing_mode) %]&nbsp;
24
    [%- END %]
25
    [%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "sales_quotation") %]
26
      <b>[%- 'Ertrag' | $T8 %]</b>&nbsp;
27
        <span name="linemargin">
28
          <span[%- IF ITEM.marge_total < 0 -%] class="plus0"[%- END -%]>
29
            [%- LxERP.format_amount(ITEM.marge_total, 2, 0) %]&nbsp;&nbsp;
30
            [%- LxERP.format_amount(ITEM.marge_percent, 2, 0) %]%
31
          </span>
32
       </span>&nbsp;
33
      <b>[%- 'LP' | $T8 %]</b>&nbsp;
34
      [%- LxERP.format_amount(ITEM.part.listprice, 2, 0) %]&nbsp;
35
      <b>[%- 'EK' | $T8 %]</b>&nbsp;
36
        [%- L.input_tag("order.orderitems[].lastcost_as_number",
37
                        ITEM.lastcost_as_number,
38
                        size = 5,
39
                        class="recalc reformat_number numeric") %]&nbsp;
40
    [%- END %]
41
    <b>[%- 'On Hand' | $T8 %]</b>&nbsp;
42
      <span class="numeric[%- IF ITEM.part.onhand < ITEM.part.rop -%] plus0[%- END -%]">
43
        [%- ITEM.part.onhand_as_number -%]&nbsp;[%- ITEM.part.unit -%]
44
      </span>&nbsp;
45
    <b>[%- 'Optional' | $T8 %]</b>&nbsp;
46
      [%- L.yes_no_tag("order.orderitems[].optional", ITEM.optional
47
                        class="recalc") %]&nbsp;
48
  </td></tr>
49

  
50
  <tr>
51
    [%- SET n = 0 %]
52
    [%- FOREACH var = ITEM.cvars_by_config %]
53
    [%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %]
54
    [%- SET n = n + 1 %]
55
    <th>
56
      [% var.config.description %]
57
    </th>
58
    <td>
59
      [% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %]
60
      [% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %]
61
      [% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %]
62
      [% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %]
63
    </td>
64
    [%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %]
65

  
66
  </tr><tr>
67
    [%- END %]
68
    [%- END %]
69
  </tr>
70
</table>
templates/webpages/invoice/tabs/_tax_row.html
1
[%- USE T8 %]
2
[%- USE HTML %]
3
[%- USE LxERP %]
4
[%- USE L %]
5

  
6
<tr class="tax_row">
7
  <th align="right">[%- IF TAXINCLUDED %][%- 'Including' | $T8 %]&nbsp;[%- END %][%- TAX.tax.taxdescription %] [% TAX.tax.rate_as_percent %]%</th>
8
  <td align="right">[%- LxERP.format_amount(TAX.amount, 2, 0) %]</td>
9
</tr>
10
[%- IF TAXINCLUDED %]
11
<tr class="tax_row">
12
  <th align="right">[%- 'Net amount' | $T8 %]</th>
13
  <td align="right">[%- LxERP.format_amount(TAX.netamount, 2, 0) %]</td>
14
</tr>
15
[%- END%]
templates/webpages/invoice/tabs/basic_data.html
1
[%- USE T8 %]
2
[%- USE HTML %]
3
[%- USE LxERP %]
4
[%- USE L %]
5
[%- USE P %]
6

  
7
[%- INCLUDE 'generic/set_longdescription.html' %]
8

  
9
<div id="ui-tabs-basic-data">
10
  <table width="100%">
11
    <tr valign="top">
12
      <td>
13
        <table width="100%">
14
          <tr>
15
            <th align="right">[%- SELF.cv == "customer" ? LxERP.t8('Customer') : LxERP.t8('Vendor') -%]</th>
16
            [% SET cv_id = SELF.cv _ '_id' %]
17
            <td>
18
              [% P.customer_vendor.picker("invoice.${SELF.cv}" _ '_id', SELF.invoice.$cv_id, type=SELF.cv, show_details="1",
19
                   style='width: 300px') %]
20
            </td>
21
          </tr>
22

  
23
          <tr id='cp_row' [%- IF !SELF.invoice.${SELF.cv}.contacts.size %]style='display:none'[%- END %]>
24
            <th align="right">[% 'Contact Person' | $T8 %]</th>
25
            <td>[% L.select_tag('invoice.cp_id',
26
                                SELF.invoice.${SELF.cv}.contacts,
27
                                default=SELF.invoice.cp_id,
28
                                title_key='full_name_dep',
29
                                value_key='cp_id',
30
                                with_empty=1,
31
                                style='width: 300px') %]</td>
32
          </tr>
33

  
34
          <tr>
35
            <th align="right">[% 'Shipping Address' | $T8 %]</th>
36
            <td>
37
              <span id='shipto_selection' [%- IF !SELF.invoice.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
38
                [% shiptos = [ { shipto_id => "", displayable_id => LxERP.t8("No/individual shipping address") } ] ;
39
                   FOREACH s = SELF.invoice.${SELF.cv}.shipto ;
40
                     shiptos.push(s) ;
41
                   END ;
42
                   L.select_tag('invoice.shipto_id',
43
                                 shiptos,
44
                                 default=SELF.invoice.shipto_id,
45
                                 title_key='displayable_id',
46
                                 value_key='shipto_id',
47
                                 with_empty=0,
48
                                 style='width: 300px') %]
49
              </span>
50
              [% L.button_tag("kivi.invoice.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
51
            </td>
52
          </tr>
53

  
54
          [%- IF SELF.cv == "customer" %]
55
          <tr id="billing_address_row"[% IF !SELF.invoice.customer.additional_billing_addresses.as_list.size %] style="display:none"[% END %]>
56
            <th align="right">[% 'Custom Billing Address' | $T8 %]</th>
57
            <td>
58
              [% L.select_tag('invoice.billing_address_id',
59
                               SELF.invoice.customer.additional_billing_addresses,
60
                               default=SELF.invoice.billing_address_id,
61
                               title_key='displayable_id',
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff