Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision f49e9deb

Von Sven Schöling vor 12 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
package SL::Controller::Invoice;
use strict;
use parent qw(SL::Controller::Base);
use SL::DB::Invoice;
use SL::Helper::Flash qw(flash flash_later);
use SL::HTML::Util;
use SL::Presenter::Tag qw(select_tag hidden_tag div_tag);
use SL::Locale::String qw(t8);
use SL::SessionFile::Random;
use SL::PriceSource;
use SL::File;
use SL::YAML;
use SL::DB::Helper::RecordLink qw(set_record_link_conversions RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
use SL::DB::Helper::TypeDataProxy;
use SL::DB::Helper::Record qw(get_object_name_from_type get_class_from_type);
use SL::Model::Record;
use SL::DB::Invoice::TypeData qw(:types);
use SL::DB::Order::TypeData qw(:types);
use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::Reclamation::TypeData qw(:types);
use SL::Helper::CreatePDF qw(:all);
use SL::Helper::PrintOptions;
use SL::Helper::ShippedQty;
use SL::Helper::UserPreferences::DisplayPreferences;
use SL::Helper::UserPreferences::PositionsScrollbar;
use SL::Helper::UserPreferences::UpdatePositions;
use SL::Controller::Helper::GetModels;
use List::Util qw(first sum0);
use List::UtilsBy qw(sort_by uniq_by);
use List::MoreUtils qw(uniq any none pairwise first_index);
use File::Spec;
use Sort::Naturally;
use Rose::Object::MakeMethods::Generic
(
scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
'scalar --get_set_init' => [ qw(invoice valid_types type cv p all_price_factors
search_cvpartnumber show_update_button
part_picker_classification_ids
type_data) ],
);
# safety
__PACKAGE__->run_before('check_auth');
__PACKAGE__->run_before('check_auth_for_edit',
except => [ qw(edit price_popup load_second_rows) ]);
#
# actions
#
# add a newinvoice
sub action_add {
my ($self) = @_;
$self->invoice(SL::Model::Record->update_after_new($self->invoice));
$self->invoice->gldate($self->invoice->payment_terms ? $self->invoice->payment_terms->calc_date(reference_date => $self->invoice->transdate) : $self->invoice->transdate);
$self->pre_render();
if (!$::form->{form_validity_token}) {
$::form->{form_validity_token} = SL::DB::ValidityToken->create(scope => SL::DB::ValidityToken::SCOPE_SALES_INVOICE_POST())->token;
}
$self->render(
'invoice/form',
title => $self->type_data->text('add'),
%{$self->{template_args}}
);
}
#
# helpers
#
sub init_valid_types {
$_[0]->type_data->valid_types;
}
sub init_type {
my ($self) = @_;
my $type = $self->invoice->record_type;
if (none { $type eq $_ } @{$self->valid_types}) {
die "Not a valid type for invoice";
}
$self->type($type);
}
sub init_cv {
my ($self) = @_;
return $self->type_data->properties('customervendor');
}
sub init_search_cvpartnumber {
my ($self) = @_;
my $user_prefs = SL::Helper::UserPreferences::PartPickerSearch->new();
my $search_cvpartnumber;
$search_cvpartnumber = !!$user_prefs->get_sales_search_customer_partnumber() if $self->cv eq 'customer';
$search_cvpartnumber = !!$user_prefs->get_purchase_search_makemodel() if $self->cv eq 'vendor';
return $search_cvpartnumber;
}
sub init_show_update_button {
my ($self) = @_;
!!SL::Helper::UserPreferences::UpdatePositions->new()->get_show_update_button();
}
sub init_p {
SL::Presenter->get;
}
sub init_invoice {
$_[0]->make_invoice;
}
sub init_all_price_factors {
SL::DB::Manager::PriceFactor->get_all;
}
sub init_part_picker_classification_ids {
my ($self) = @_;
return [ map { $_->id } @{ SL::DB::Manager::PartClassification->get_all(
where => $self->type_data->part_classification_query()) } ];
}
sub init_type_data {
my ($self) = @_;
SL::DB::Helper::TypeDataProxy->new('SL::DB::Invoice', $self->invoice->record_type);
}
sub check_auth {
my ($self) = @_;
$::auth->assert($self->type_data->rights('view'));
}
sub check_auth_for_edit {
my ($self) = @_;
$::auth->assert($self->type_data->rights('edit'));
}
#
# internal
#
sub pre_render {
my ($self) = @_;
$self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
$self->{all_currencies} = SL::DB::Manager::Currency->get_all_sorted();
$self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
$self->{all_languages} = SL::DB::Manager::Language->get_all_sorted( query => [ or => [ obsolete => 0, id => $self->invoice->language_id ] ] );
$self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->employee_id,
deleted => 0 ] ],
sort_by => 'name');
$self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->invoice->salesman_id,
deleted => 0 ] ],
sort_by => 'name');
$self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->invoice->payment_id,
obsolete => 0 ] ]);
$self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_valid($self->invoice->delivery_term_id);
$self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
$self->{positions_scrollbar_height} = SL::Helper::UserPreferences::PositionsScrollbar->new()->get_height();
my $print_form = Form->new('');
$print_form->{type} = $self->type;
$print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
$self->{print_options} = SL::Helper::PrintOptions->get_print_options(
form => $print_form,
options => {dialog_name_prefix => 'print_options.',
show_headers => 1,
no_queue => 1,
no_postscript => 1,
no_opendocument => 0,
no_html => 0},
);
foreach my $item (@{$self->invoice->items}) {
my $price_source = SL::PriceSource->new(record_item => $item, record => $self->invoice);
$item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
$item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
}
# ?! TODO: does invoices need stock info?
# 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())) {
# # Calculate shipped qtys here to prevent calling calculate for every item via the items method.
# # Do not use write_to_objects to prevent order->delivered to be set, because this should be
# # the value from db, which can be set manually or is set when linked delivery orders are saved.
# SL::Helper::ShippedQty->new->calculate($self->record)->write_to(\@{$self->order->items});
# }
if ($self->invoice->number && $::instance_conf->get_webdav) {
my $webdav = SL::Webdav->new(
type => $self->type,
number => $self->invoice->number,
);
my @all_objects = $webdav->get_all_objects;
@{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
type => t8('File'),
link => File::Spec->catfile($_->full_filedescriptor),
} } @all_objects;
}
# if ( (any { $self->type eq $_ } (SALES_QUOTATION_TYPE(), SALES_ORDER_INTAKE_TYPE(), SALES_ORDER_TYPE()))
# && $::instance_conf->get_transport_cost_reminder_article_number_id ) {
# $self->{template_args}->{transport_cost_reminder_article} = SL::DB::Part->new(id => $::instance_conf->get_transport_cost_reminder_article_number_id)->load;
# }
$self->{template_args}->{longdescription_dialog_size_percentage} = SL::Helper::UserPreferences::DisplayPreferences->new()->get_longdescription_dialog_size_percentage();
$self->get_item_cvpartnumber($_) for @{$self->invoice->items_sorted};
# $self->{template_args}->{num_phone_notes} = scalar @{ $self->order->phone_notes || [] };
$::request->{layout}->use_javascript("${_}.js") for qw(kivi.Validator kivi.SalesPurchase kivi.Invoice kivi.File
calculate_qty follow_up show_history);
$self->setup_edit_action_bar;
}
sub setup_edit_action_bar {
my ($self, %params) = @_;
my $change_never = $::instance_conf->get_is_changeable == 0;
my $change_on_same_day_only = $::instance_conf->get_is_changeable == 2 && $self->invoice->gldate->clone->truncate(to => 'day') != DateTime->today;
my $payments_balanced = 0; #($::form->{oldtotalpaid} == 0); # TODO: don't rely on form
my $has_storno = 0; # $self->invoice->linked_record('storno'); # TODO: linked record?
my $may_edit_create = $::auth->assert($self->type_data->rights('edit'), 'may fail');
my $factur_x_enabled = $self->invoice->customer && $self->invoice->customer->create_zugferd_invoices_for_this_customer;
my ($is_linked_bank_transaction, $warn_unlinked_delivery_order);
if ($::form->{id}
&& SL::DB::Default->get->payments_changeable != 0
&& SL::DB::Manager::BankTransactionAccTrans->find_by(ar_id => $::form->{id})) {
$is_linked_bank_transaction = 1;
}
if ($::instance_conf->get_warn_no_delivery_order_for_invoice && !$self->invoice->id) {
$warn_unlinked_delivery_order = 1 unless $::form->{convert_from_do_ids};
}
my $has_further_invoice_for_advance_payment;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
$has_further_invoice_for_advance_payment = any {'SL::DB::Invoice' eq ref $_ && "invoice_for_advance_payment" eq $_->type} @$lr;
}
my $has_final_invoice;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'to', to => ['Invoice']);
$has_final_invoice = any {'SL::DB::Invoice' eq ref $_ && "final_invoice" eq $_->invoice_type} @$lr;
}
my $is_invoice_for_advance_payment_from_order;
if ($self->invoice->id && $self->invoice->is_type(INVOICE_FOR_ADVANCE_PAYMENT_TYPE())) {
my $lr = $self->invoice->linked_records(direction => 'from', from => ['Order']);
$is_invoice_for_advance_payment_from_order = scalar @$lr >= 1;
}
my $locked = 0; # TODO: get from... somewhere
# add readonly state in tmpl_vars
# $tmpl_var->{readonly} = !$may_edit_create ? 1
# : $form->{locked} ? 1
# : $form->{storno} ? 1
# : ($form->{id} && $change_never) ? 1
# : ($form->{id} && $change_on_same_day_only) ? 1
# : $is_linked_bank_transaction ? 1
# : 0;
for my $bar ($::request->layout->get('actionbar')) {
$bar->add(
action => [
t8('Update'),
submit => [ '#form', { action => "update" } ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: undef,
id => 'update_button',
accesskey => 'enter',
],
combobox => [
action => [
t8('Post'),
submit => [ '#form', { action => "post" } ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: $self->invoice->id && $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [
t8('Post and Close'),
submit => [ '#form', { action => "post_and_close" } ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: $self->invoice->id && $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $self->invoice->id && $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [
t8('Post Payment'),
submit => [ '#form', { action => "post_payment" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
only_if => $self->invoice->record_type ne "invoice_for_advance_payment",
],
action => [ t8('Mark as paid'),
submit => [ '#form', { action => "mark_as_paid" } ],
confirm => t8('This will remove the invoice from showing as unpaid even if the unpaid amount does not match the amount. Proceed?'),
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: undef,
only_if => ($::instance_conf->get_is_show_mark_as_paid && $self->invoice->record_type ne "invoice_for_advance_payment")
|| $self->invoice->record_type eq 'final_invoice',
],
], # end of combobox "Post"
combobox => [
action => [ t8('Storno'),
submit => [ '#form', { action => "storno" } ],
confirm => t8('Do you really want to cancel this invoice?'),
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self->invoice->storno ? t8('Cannot storno storno invoice!')
: !$payments_balanced ? t8('Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount')
: undef,
],
action => [ t8('Delete'),
submit => [ '#form', { action => "delete" } ],
confirm => t8('Do you really want to delete this object?'),
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $locked ? t8('The billing period has already been locked.')
: $change_never ? t8('Changing invoices has been disabled in the configuration.')
: $change_on_same_day_only ? t8('Invoices can only be changed on the day they are posted.')
: $has_storno ? t8('Can only delete the "Storno zu" part of the cancellation pair.')
: undef,
],
], # end of combobox "Storno"
'separator',
combobox => [
action => [ t8('Workflow') ],
action => [
t8('Use As New'),
submit => [ '#form', { action => "use_as_new" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: undef,
],
action => [
t8('Further Invoice for Advance Payment'),
submit => [ '#form', { action => "further_invoice_for_advance_payment" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $has_further_invoice_for_advance_payment ? t8('This invoice has already a further invoice for advanced payment.')
: $has_final_invoice ? t8('This invoice has already a final invoice.')
: $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
: undef,
only_if => $self->invoice->record_type eq "invoice_for_advance_payment",
],
action => [
t8('Final Invoice'),
submit => [ '#form', { action => "final_invoice" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $has_further_invoice_for_advance_payment ? t8('This invoice has a further invoice for advanced payment.')
: $has_final_invoice ? t8('This invoice has already a final invoice.')
: $is_invoice_for_advance_payment_from_order ? t8('This invoice was added from an order. See there.')
: undef,
only_if => $self->invoice->is_type("invoice_for_advance_payment"),
],
action => [
t8('Credit Note'),
submit => [ '#form', { action => "credit_note" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $self->invoice->is_type("credit_note") ? t8('Credit notes cannot be converted into other credit notes.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self>invocie->storno ? t8('A canceled invoice cannot be used. Please undo the cancellation first.')
: undef,
],
action => [
t8('Sales Order'),
submit => [ '#form', { action => "order" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Reclamation'),
submit => ['#form', { action => "sales_reclamation" }], # can't call Reclamation directly
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
only_if => ($self->invoice->is_type('invoice') && !$::form->{storno}),
],
], # end of combobox "Workflow"
combobox => [
action => [ t8('Export') ],
action => [
($self->invoice->id ? t8('Print') : t8('Preview')),
call => [ 'kivi.SalesPurchase.show_print_dialog', $self->invoice->id ? 'print' : 'preview' ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id && $locked ? t8('The billing period has already been locked.')
: undef,
],
action => [ t8('Print and Post'),
call => [ 'kivi.SalesPurchase.show_print_dialog', 'print_and_post' ],
checks => [ 'kivi.validate_form' ],
confirm => t8('The invoice is not linked with a sales delivery order. Post anyway?') x !!$warn_unlinked_delivery_order,
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $locked ? t8('The billing period has already been locked.')
: $self->invoice->storno ? t8('A canceled invoice cannot be posted.')
: ($self->invoice->id && $change_never) ? t8('Changing invoices has been disabled in the configuration.')
: ($self->invoice->id && $change_on_same_day_only) ? t8('Invoices can only be changed on the day they are posted.')
: $is_linked_bank_transaction ? t8('This transaction is linked with a bank transaction. Please undo and redo the bank transaction booking if needed.')
: undef,
],
action => [ t8('E Mail'),
call => [ 'kivi.SalesPurchase.show_email_dialog' ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: $self->invoice->customer->postal_invoice ? t8('This customer wants a postal invoices.')
: undef,
],
action => [ t8('Factur-X/ZUGFeRD'),
submit => [ '#form', { action => "download_factur_x_xml" } ],
checks => [ 'kivi.validate_form' ],
disabled => !$may_edit_create ? t8('You must not print this invoice.')
: !$self->invoice->id ? t8('This invoice has not been posted yet.')
: !$factur_x_enabled ? t8('Creating Factur-X/ZUGFeRD invoices is not enabled for this customer.')
: undef,
],
], # end of combobox "Export"
combobox => [
action => [ t8('more') ],
action => [
t8('History'),
call => [ 'set_history_window', $self->invoice->id * 1, 'glid' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Follow-Up'),
call => [ 'follow_up_window' ],
disabled => !$self->invoice->id ? t8('This invoice has not been posted yet.') : undef,
],
action => [
t8('Drafts'),
call => [ 'kivi.Draft.popup', 'is', 'invoice', $::form->{draft_id}, $::form->{draft_description} ],
disabled => !$may_edit_create ? t8('You must not change this invoice.')
: $self->invoice->id ? t8('This invoice has already been posted.')
: $locked ? t8('The billing period has already been locked.')
: undef,
],
], # end of combobox "more"
);
}
}
# load or create a new order object
#
# And assign changes from the form to this object.
# If the order is loaded from db, check if items are deleted in the form,
# remove them form the object and collect them for removing from db on saving.
# Then create/update items from form (via make_item) and add them.
sub make_invoice {
my ($self) = @_;
# add_items adds items to an order with no items for saving, but they cannot
# be retrieved via items until the order is saved. Adding empty items to new
# order here solves this problem.
my $invoice;
$invoice = SL::DB::Invoice->new(id => $::form->{id})->load(with => [ 'invoiceitems', 'invoiceitems.part' ]) if $::form->{id};
$invoice ||= SL::DB::Invoice->new(invoiceitems => [],
record_type => $::form->{type},
currency_id => $::instance_conf->get_currency_id(),);
my $cv_id_method = $invoice->type_data->properties('customervendor'). '_id';
if (!$::form->{id} && $::form->{$cv_id_method}) {
$invoice->$cv_id_method($::form->{$cv_id_method});
$invoice = SL::Model::Record->update_after_customer_vendor_change($invoice);
}
my $form_invoiceitems = delete $::form->{invoice}->{invoiceitems};
$invoice->assign_attributes(%{$::form->{invoice}});
$self->setup_custom_shipto_from_form($invoice, $::form);
# remove deleted items
$self->item_ids_to_delete([]);
foreach my $idx (reverse 0..$#{$invoice->invoiceitems}) {
my $item = $invoice->invoiceitems->[$idx];
if (none { $item->id == $_->{id} } @{$form_invoiceitems}) {
splice @{$invoice->invoiceitems}, $idx, 1;
push @{$self->item_ids_to_delete}, $item->id;
}
}
my @items;
my $pos = 1;
foreach my $form_attr (@{$form_invoiceitems}) {
my $item = make_item($invoice, $form_attr);
$item->position($pos);
push @items, $item;
$pos++;
}
$invoice->add_items(grep {!$_->id} @items);
return $invoice;
}
# create or update items from form
#
# Make item objects from form values. For items already existing read from db.
# Create a new item else. And assign attributes.
sub make_item {
my ($record, $attr) = @_;
my $item;
$item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
my $is_new = !$item;
# add_custom_variables adds cvars to an orderitem with no cvars for saving, but
# they cannot be retrieved via custom_variables until the order/orderitem is
# saved. Adding empty custom_variables to new orderitem here solves this problem.
$item ||= SL::DB::InvoiceItem->new(custom_variables => []);
$item->assign_attributes(%$attr);
if ($is_new) {
my $texts = get_part_texts($item->part, $record->language_id);
$item->longdescription($texts->{longdescription}) if !defined $attr->{longdescription};
$item->project_id($record->globalproject_id) if !defined $attr->{project_id};
$item->lastcost($record->is_sales ? $item->part->lastcost : 0) if !defined $attr->{lastcost_as_number};
}
return $item;
}
# setup custom shipto from form
#
# The dialog returns form variables starting with 'shipto' and cvars starting
# with 'shiptocvar_'.
# Mark it to be deleted if a shipto from master data is selected
# (i.e. order has a shipto).
# Else, update or create a new custom shipto. If the fields are empty, it
# will not be saved on save.
sub setup_custom_shipto_from_form {
my ($self, $record, $form) = @_;
if ($record->shipto) {
$self->is_custom_shipto_to_delete(1);
} else {
my $custom_shipto = $record->custom_shipto || $record->custom_shipto(SL::DB::Shipto->new(module => 'AR', custom_variables => []));
my $shipto_cvars = {map { my ($key) = m{^shiptocvar_(.+)}; $key => delete $form->{$_}} grep { m{^shiptocvar_} } keys %$form};
my $shipto_attrs = {map { $_ => delete $form->{$_}} grep { m{^shipto} } keys %$form};
$custom_shipto->assign_attributes(%$shipto_attrs);
$custom_shipto->cvar_by_name($_)->value($shipto_cvars->{$_}) for keys %$shipto_cvars;
}
}
1;
SL/DB/Invoice.pm
use SL::DB::Helper::RecordLink qw(RECORD_ID RECORD_TYPE_REF RECORD_ITEM_ID RECORD_ITEM_TYPE_REF);
use SL::DB::Helper::SalesPurchaseInvoice;
use SL::DB::Helper::TransNumberGenerator;
use SL::DB::Helper::TypeDataProxy;
use SL::DB::Helper::ZUGFeRD qw(:CREATE);
use SL::Locale::String qw(t8);
......
__PACKAGE__->before_save('_before_save_set_invnumber');
__PACKAGE__->after_save('_after_save_link_records');
use Rose::Object::MakeMethods::Generic (
'scalar --get_set_init' => [ qw(record_type) ],
);
# hooks
sub _before_save_set_invnumber {
......
sub items { goto &invoiceitems; }
sub add_items { goto &add_invoiceitems; }
sub record_number { goto &invnumber; };
sub record_type { goto &invoice_type; };
#sub record_type { goto &invoice_type; };
sub is_sales {
# For compatibility with Order, DeliveryOrder
......
}
}
sub init_record_type {
goto &invoice_type;
}
sub invoice_type {
my ($self) = @_;
......
goto &customer;
}
sub is_type {
return shift->record_type eq shift;
}
sub number {
my $self = shift;
my $nr_key = $self->type_data->properties('nr_key');
return $self->$nr_key(@_);
}
sub link {
my ($self) = @_;
......
return $self->netamount; # already matches base currency
}
sub type_data {
SL::DB::Helper::TypeDataProxy->new(ref $_[0], $_[0]->record_type);
}
1;
__END__
SL/DB/Invoice/TypeData.pm
worflow_needed => 0,
},
defaults => {
# TODO
# aka: duedate
reqdate => sub {
#$invoice->payment_terms ? $invoice->payment_terms->calc_date(reference_date => $::form->{invdate})->to_kivitendo : $invoice->transdate;
DateTime->today
},
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
edit => "invoice_edit",
view => "invoice_edit | sales_invoice_view",
},
features => {
price_tax => 1,
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
......
defaults => {
# TODO
},
part_classification_query => [ "used_for_sales" => 1 ],
part_classification_query => [ "used_for_sale" => 1 ],
rights => {
# TODO
},
SL/Model/Record.pm
$new_record->transdate(DateTime->now_local());
my $default_reqdate = $new_record->type_data->defaults('reqdate');
$new_record->reqdate($default_reqdate);
if ($new_record->can('reqdate')) {
my $default_reqdate = $new_record->type_data->defaults('reqdate');
$new_record->reqdate($default_reqdate);
}
return $new_record;
}
templates/webpages/invoice/form.html
[%- USE T8 %]
[%- USE LxERP %]
[%- USE L %]
[%- USE HTML %]
[% IF FORM.workflow_email_journal_id %]
[% SET FORM.title = LxERP.t8("Email Journal Workflow") _ " - " _ FORM.title %]
[% END %]
<h1>[% FORM.title %] <span id='nr_in_title'>[%- SELF.invoice.number -%]</span></h1>
<div id="print_options" style="display:none">
<form method="post" action="controller.pl" id="print_options_form">
[% SELF.print_options %]
<br>
[% L.button_tag('kivi.Order.print()', LxERP.t8('Print')) %]
<a href="#" onclick="$('#print_options').dialog('close');">[% LxERP.t8("Cancel") %]</a>
</form>
</div>
<div id="shipto_dialog" class="hidden"></div>
<form method="post" action="controller.pl" id="invoice_form"
data-transport-cost-reminder-article-id="[% HTML.escape(transport_cost_reminder_article.id) %]"
data-transport-cost-reminder-article-description="[% HTML.escape(transport_cost_reminder_article.displayable_name) %]">
[% L.hidden_tag('callback', FORM.callback) %]
[% L.hidden_tag('type', FORM.type) %]
[% L.hidden_tag('id', SELF.invoice.id) %]
[% L.hidden_tag('converted_from_record_type_ref', SELF.invoice.converted_from_record_type_ref) %]
[% L.hidden_tag('converted_from_record_id', SELF.invoice.converted_from_record_id) %]
[% L.hidden_tag('email_journal_id', FORM.email_journal_id) %]
[% L.hidden_tag('email_attachment_id', FORM.email_attachment_id) %]
[% L.hidden_tag('workflow_email_journal_id', FORM.workflow_email_journal_id) %]
[% L.hidden_tag('workflow_email_attachment_id', FORM.workflow_email_attachment_id) %]
[% L.hidden_tag('workflow_email_callback', FORM.workflow_email_callback) %]
[% IF !SELF.invoice.id %]
[% L.hidden_tag('form_validity_token', FORM.form_validity_token) %]
[% END %]
[%- INCLUDE 'common/flash.html' %]
<div class="tabwidget" id="invoice_tabs">
<ul>
<li><a href="#ui-tabs-basic-data">[% 'Basic Data' | $T8 %]</a></li>
[% IF FORM.email_attachment_id || FORM.workflow_email_attachment_id %]
<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>
[% END %]
[%- IF INSTANCE_CONF.get_webdav %]
<li><a href="#ui-tabs-webdav">[% 'WebDAV' | $T8 %]</a></li>
[%- END %]
[%- IF SELF.invoice.id AND INSTANCE_CONF.get_doc_storage %]
<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>
<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>
[%- END %]
[%- IF SELF.invoice.id %]
<li><a href="controller.pl?action=RecordLinks/ajax_list&object_model=Invoice&object_id=[% HTML.url(SELF.invoice.id) %]">[% 'Linked Records' | $T8 %]</a></li>
[%- END %]
[% IF SELF.invoice.id %]
<li><a href="#ui-tabs-phone-notes">[% 'Phone Notes' | $T8 %]<span id="num_phone_notes">[%- num_phone_notes ? ' (' _ num_phone_notes _ ')' : '' -%]</span></a></li>
[% END %]
</ul>
[% PROCESS "invoice/tabs/basic_data.html" %]
[% PROCESS 'webdav/_list.html' %]
<div id="ui-tabs-1">
[%- LxERP.t8("Loading...") %]
</div>
[% IF SELF.invoice.id %]
<div id="ui-tabs-phone-notes">
[% PROCESS "invoice/tabs/phone_notes.html" %]
</div>
[% END %]
<div id="shipto_inputs" class="hidden">
[%- PROCESS 'common/_ship_to_dialog.html'
vc_obj=SELF.invoice.customervendor
cs_obj=SELF.invoice.custom_shipto
cvars=SELF.invoice.custom_shipto.cvars_by_config
id_selector='#invoice_shipto_id' %]
</div>
</div>
</form>
templates/webpages/invoice/tabs/_business_info_row.html
[%- USE T8 %][%- USE HTML %]
<tr id='business_info_row' [%- IF !SELF.order.customervendor.business_id %]style='display:none'[%- END %]>
<th align="right">[%- IF SELF.cv == 'customer' -%]
[%- 'Customer type' | $T8 -%]
[%- ELSE -%]
[%- 'Vendor type' | $T8 -%]
[%- END -%]</th>
<td>[% HTML.escape(SELF.order.customervendor.business.description) %]; [% 'Trade Discount' | $T8 %] [% SELF.order.customervendor.business.discount_as_percent %] %</td>
</tr>
templates/webpages/invoice/tabs/_item_input.html
[%- USE T8 %][%- USE HTML %][%- USE LxERP %][%- USE L %][%- USE P %]
<div>
<table id="input_row_table_id">
<thead>
<tr class="listheading">
<th class="listheading" nowrap >[%- '+' | $T8 %] </th>
<th class="listheading" nowrap >[%- 'position' | $T8 %] </th>
<th class="listheading" nowrap >[%- 'Part' | $T8 %] </th>
<th class="listheading" nowrap >[%- 'Description' | $T8 %] </th>
<th class="listheading" nowrap width="5" >[%- 'Qty' | $T8 %] </th>
<th class="listheading" nowrap width="15">[%- 'Price' | $T8 %] </th>
<th class="listheading" nowrap width="5" >[%- 'Discount' | $T8 %] </th>
<th></th>
</tr>
</thead>
<tbody>
<tr valign="top" class="listrow">
<td class="tooltipster-html" title="[%- 'Create a new part' | $T8 -%]">
[% SET type_options = [[ 'part', LxERP.t8('Part') ], [ 'assembly', LxERP.t8('Assembly') ], [ 'service', LxERP.t8('Service') ] ] %]
[%- IF INSTANCE_CONF.get_feature_experimental_assortment %]
[%- type_options.push([ 'assortment', LxERP.t8('Assortment')]) %]
[%- END %]
[% L.select_tag('add_item.create_part_type', type_options) %]
[% L.button_tag('kivi.Order.create_part()', LxERP.t8('+')) %]
</td>
<td>[% L.input_tag('add_item.position', '', size = 5, class="add_item_input numeric") %]</td>
<td>
[%- SET PARAM_KEY = SELF.cv == "customer" ? 'with_customer_partnumber' : 'with_makemodel' -%]
[%- SET PARAM_VAL = SELF.search_cvpartnumber -%]
[% P.part.picker('add_item.parts_id', SELF.created_part, style='width: 300px', class="add_item_input",
multiple_pos_input=1,
action={set_multi_items='kivi.Order.add_multi_items'},
classification_id=SELF.part_picker_classification_ids.as_list.join(','),
$PARAM_KEY=PARAM_VAL) %]</td>
<td>[% L.input_tag('add_item.description', SELF.created_part.description, class="add_item_input") %]</td>
<td>
[% L.input_tag('add_item.qty_as_number', '', placeholder="1", size = 5, class="add_item_input numeric") %]
[% L.hidden_tag('add_item.unit', SELF.created_part.unit, class="add_item_input") %]
</td>
[%- SET price = '' %]
[%- IF SELF.created_part %]
[%- 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) -%]
[%- END %]
<td>[% L.input_tag('add_item.sellprice_as_number', price, size = 10, class="add_item_input numeric tooltipster-html") %]</td>
<td>[% L.input_tag('add_item.discount_as_percent', '', size = 5, class="add_item_input numeric tooltipster-html") %]</td>
<td>[% L.button_tag('kivi.Order.add_item()', LxERP.t8('Add part')) %]</td>
</tr>
</tbody>
</table>
</div>
templates/webpages/invoice/tabs/_price_sources_dialog.html
[%- USE T8 %]
[%- USE HTML %]
[%- USE L %]
[%- USE LxERP %]
[% SET best_price = price_source.best_price %]
[% SET best_discount = price_source.best_discount %]
[% SET price_editable = 0 %]
[% IF (FORM.type == "sales_order" || FORM.type == "sales_order_intake" || FORM.type == "sales_quotation") %]
[% SET price_editable = AUTH.assert('sales_edit_prices', 1) %]
[% END %]
[% IF (FORM.type == "purchase_order" || FORM.type == "purchase_order_confirmation" || FORM.type == "request_quotation" || FORM.type == "purchase_quotation_intake") %]
[% SET price_editable = AUTH.assert('purchase_edit_prices', 1) %]
[% END %]
[% SET exfactor = price_source.record.exchangerate ? 1 / price_source.record.exchangerate : 1 %]
[% SET exnoshow = price_source.record.currency_id==INSTANCE_CONF.get_currency_id %]
[% SET places = exnoshow ? -2 : 5 %]
<h2>[% 'Prices' | $T8 %]</h2>
<table>
<tr class='listheading'>
<th></th>
<th>[% 'Price Source' | $T8 %]</th>
<th>[% 'Price' | $T8 %]</th>
<th [%- IF exnoshow -%]style='display:none'[%- END %]>
[% 'Price' | $T8 -%]/[%- price_source.record.currency.name %]
</th>
<th>[% 'Best Price' | $T8 %]</th>
<th>[% 'Details' | $T8 %]</th>
</tr>
<tr class='listrow'>
[%- IF price_source.record_item.active_price_source %]
<td>[% L.button_tag('kivi.Order.update_price_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
[%- ELSE %]
<td><b>[% 'Selected' | $T8 %]</b></td>
[%- END %]
<td>[% 'None (PriceSource)' | $T8 %]</td>
<td>-</td>
<td [%- IF exnoshow -%]style='display:none'[%- END %]>-</td>
<td></td>
<td></td>
</tr>
[%- FOREACH price IN price_source.available_prices %]
<tr class='listrow'>
[%- IF price_source.record_item.active_price_source != price.source %]
<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>
[%- ELSIF price_source.record_item.sellprice * 1 != price.price * 1 %]
<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>
[%- ELSE %]
<td><b>[% 'Selected' | $T8 %]</b></td>
[% END %]
<td>[% price.source_description | html %]</td>
<td>[% price.price_as_number %]</td>
<td [%- IF exnoshow -%]style='display:none'[%- END %]>
[% LxERP.format_amount(price.price * exfactor, places) %]
</td>
[% IF price.source == best_price.source %]
<td align='center'>&#x2022;</td>
[% ELSE %]
<td></td>
[% END %]
<td>[% price.description | html %]</td>
</tr>
[%- END %]
</table>
<h2>[% 'Discounts' | $T8 %]</h2>
<table>
<tr class='listheading'>
<th></th>
<th>[% 'Price Source' | $T8 %]</th>
<th>[% 'Discount' | $T8 %]</th>
<th>[% 'Best Discount' | $T8 %]</th>
<th>[% 'Details' | $T8 %]</th>
</tr>
<tr class='listrow'>
[%- IF price_source.record_item.active_discount_source %]
<td>[% L.button_tag('kivi.Order.update_discount_source(\'' _ FORM.item_id _ '\', \'\', \'' _ LxERP.t8('None (PriceSource Discount)') _ '\', \'\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
[%- ELSE %]
<td><b>[% 'Selected' | $T8 %]</b></td>
[%- END %]
<td>[% 'None (PriceSource Discount)' | $T8 %]</td>
<td>-</td>
<td></td>
<td></td>
</tr>
[%- FOREACH price IN price_source.available_discounts %]
<tr class='listrow'>
[%- IF price_source.record_item.active_discount_source != price.source %]
<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>
[%- ELSIF price_source.record_item.discount * 1 != price.discount * 1 %]
<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>
[%- ELSE %]
<td><b>[% 'Selected' | $T8 %]</b></td>
[% END %]
<td>[% price.source_description | html %]</td>
<td>[% price.discount_as_percent %] %</td>
[% IF price.source == best_discount.source %]
<td align='center'>&#x2022;</td>
[% ELSE %]
<td></td>
[% END %]
<td>[% price.description | html %]</td>
</tr>
[%- END %]
</table>
templates/webpages/invoice/tabs/_purchase_delivery_order_item_selection.html
[%- USE LxERP -%][%- USE L -%][%- USE HTML -%]
<div>
[% L.button_tag("kivi.Order.convert_to_purchase_delivery_order_item_selection()", LxERP.t8("Create delivery order")) %]
</div>
<div>
<table>
<thead>
<tr class="listheading">
<th>[% L.checkbox_tag("item_position_selection_checkall", checkall="input.item_position_selection_checkall", checked="checked") %]</th>
<th>[% LxERP.t8("Position") %]</th>
<th>[% LxERP.t8("Partnumber") %]</th>
<th>[% LxERP.t8("Vendor Part Number") %]</th>
<th>[% LxERP.t8("Description") %]</th>
<th>[% LxERP.t8("Qty") %]</th>
<th>[% LxERP.t8("Unit") %]</th>
</tr>
</thead>
<tbody id="item_position_selection" class="row_entry listrow">
[% FOREACH item = ITEMS %]
<tr class="listrow">
[% SET pos = loop.count - 1 %]
<td>[% L.checkbox_tag("selected_item_positions[+]", class="item_position_selection_checkall", value=loop.count - 1, checked="checked") %]</td>
<td align="right">[% loop.count %]</td>
<td>[% HTML.escape(item.partnumber) %]</td>
<td>[% HTML.escape(item.vendor_partnumber) %]</td>
<td>[% HTML.escape(item.description) %]</td>
<td align="right">[% HTML.escape(item.qty_as_number) %]</td>
<td>[% HTML.escape(item.unit) %]</td>
</tr>
[% END %]
</tbody>
</table>
</div>
templates/webpages/invoice/tabs/_row.html
[%- USE T8 %]
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
[%- USE P %]
<tbody class="row_entry listrow" data-position="[%- HTML.escape(ITEM.position) -%]"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
<tr>
<td align="center">
[%- IF MYCONFIG.show_form_details %]
[% L.img_tag(src="image/collapse.svg",
alt=LxERP.t8('Hide details'), title=LxERP.t8('Hide details'), class="expand") %]
[%- ELSE %]
[% L.img_tag(src="image/expand.svg",
alt=LxERP.t8('Show details'), title=LxERP.t8('Show details'), class="expand") %]
[%- END %]
[% L.hidden_tag("orderitem_ids[+]", ID) %]
[% L.hidden_tag("converted_from_record_item_type_ref[+]", ITEM.converted_from_record_item_type_ref) %]
[% L.hidden_tag("converted_from_record_item_id[+]", ITEM.converted_from_record_item_id) %]
[% L.hidden_tag("order.orderitems[+].id", ITEM.id, id='item_' _ ID) %]
[% L.hidden_tag("order.orderitems[].parts_id", ITEM.parts_id) %]
[% L.hidden_tag("purchase_basket_item_ids[+]", ITEM.purchase_basket_item_ids) %]
</td>
<td>
<div name="position" class="numeric">
[% HTML.escape(ITEM.position) %]
</div>
</td>
<td align="center">
<img src="image/updown.png" alt="[%- LxERP.t8('reorder item') %]" class="dragdrop">
</td>
<td align="center">
[%- L.button_tag("kivi.Order.delete_order_item_row(this)",
LxERP.t8("X"),
confirm=LxERP.t8("Are you sure?")) %]
</td>
[%- IF SELF.show_update_button -%]
<td align="center">
[%- L.img_tag(src="image/rotate_cw.svg",
alt=LxERP.t8('Update from master data'),
title= LxERP.t8('Update from master data'),
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);",
id='update_from_master') %]
</td>
[%- END -%]
<td>
<div name="partnumber">
[%- 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')) -%]
</div>
</td>
[%- IF SELF.search_cvpartnumber -%]
<td>
<div name="cvpartnumber">[% HTML.escape(ITEM.cvpartnumber) %]</div>
</td>
[%- END -%]
<td>
<div name="partclassification">[% ITEM.part.presenter.typeclass_abbreviation %]</div>
</td>
<td>
[% L.areainput_tag("order.orderitems[].description",
ITEM.description,
size='40',
style='width: 300px') %]
[%- L.hidden_tag("order.orderitems[].longdescription", ITEM.longdescription) %]
[%- L.button_tag("kivi.Order.show_longdescription_dialog(this)", LxERP.t8("L")) %]
</td>
[%- IF (SELF.type == "sales_order_intake" || SELF.type == "sales_order" || SELF.type == "purchase_order" || SELF.type == "purchase_order_confirmation") -%]
<td nowrap>
[%- L.div_tag(LxERP.format_amount(ITEM.shipped_qty, 2, 0) _ ' ' _ ITEM.unit, name="shipped_qty", class="numeric") %]
</td>
[%- END -%]
<td nowrap>
[%- L.input_tag("order.orderitems[].qty_as_number",
ITEM.qty_as_number,
size = 5,
class="recalc reformat_number numeric") %]
[%- IF ITEM.part.formel -%]
[%- L.button_tag("kivi.Order.show_calculate_qty_dialog(this)", LxERP.t8("*/")) %]
[%- L.hidden_tag("formula[+]", ITEM.part.formel) -%]
[%- END -%]
</td>
<td>
[%- L.select_tag("order.orderitems[].price_factor_id",
SELF.all_price_factors,
default = ITEM.price_factor_id,
title_key = 'description',
with_empty = 1,
class="recalc") %]
</td>
<td nowrap>
[%- L.select_tag("order.orderitems[].unit",
ITEM.part.available_units,
default = ITEM.unit,
title_key = 'name',
value_key = 'name',
class = 'unitselect') %]
</td>
<td>
[%- L.button_tag("kivi.Order.price_chooser_item_row(this)",
ITEM.active_price_source.source_description _ ' | ' _ ITEM.active_discount_source.source_description,
name = "price_chooser_button") %]
</td>
[% SET RIGHT_TO_EDIT_PRICES = 0 %]
[% IF (SELF.type == "sales_order_intake" || SELF.type == "sales_order" || SELF.type == "sales_quotation") %]
[% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('sales_edit_prices', 1) %]
[% END %]
[% IF (SELF.type == "purchase_order" || SELF.type == "purchase_order_confirmation" || SELF.type == "request_quotation" || SELF.type == "purchase_quotation_intake") %]
[% SET RIGHT_TO_EDIT_PRICES = AUTH.assert('purchase_edit_prices', 1) %]
[% END %]
<td>
[%- L.hidden_tag("order.orderitems[].active_price_source", ITEM.active_price_source.source) %]
[%- SET EDIT_PRICE = (RIGHT_TO_EDIT_PRICES && ITEM.active_price_source.source == '') %]
<div name="editable_price" [%- IF !EDIT_PRICE %]style="display:none"[%- END %] class="numeric">
[%- L.input_tag("order.orderitems[].sellprice_as_number",
ITEM.sellprice_as_number,
size = 10,
disabled=(EDIT_PRICE? '' : 1),
class="recalc reformat_number numeric") %]
</div>
<div name="not_editable_price" [%- IF EDIT_PRICE %]style="display:none"[%- END %]>
[%- L.div_tag(ITEM.sellprice_as_number, name="sellprice_text", class="numeric") %]
[%- L.hidden_tag("order.orderitems[].sellprice_as_number",
ITEM.sellprice_as_number,
disabled=(EDIT_PRICE? 1 : '')) %]
</div>
</td>
<td>
[%- L.hidden_tag("order.orderitems[].active_discount_source", ITEM.active_discount_source.source) %]
[%- SET EDIT_DISCOUNT = (RIGHT_TO_EDIT_PRICES && ITEM.active_discount_source.source == '') %]
<div name="editable_discount" [%- IF !EDIT_DISCOUNT %]style="display:none"[%- END %] class="numeric">
[%- L.input_tag("order.orderitems[].discount_as_percent",
ITEM.discount_as_percent,
size = 5,
disabled=(EDIT_DISCOUNT? '' : 1),
class="recalc reformat_number numeric") %]
</div>
<div name="not_editable_discount" [%- IF EDIT_DISCOUNT %]style="display:none"[%- END %]>
[%- L.div_tag(ITEM.discount_as_percent, name="discount_text", class="numeric") %]
[%- L.hidden_tag("order.orderitems[].discount_as_percent",
ITEM.discount_as_percent,
disabled=(EDIT_DISCOUNT? 1 : '')) %]
</div>
</td>
<td align="right">
[%- L.div_tag(LxERP.format_amount(ITEM.linetotal, 2, 0), name="linetotal") %]
</td>
</tr>
<tr [%- IF !MYCONFIG.show_form_details -%]style="display:none"[%- END -%]>
<td colspan="100%">
[%- IF MYCONFIG.show_form_details || ITEM.render_second_row %]
<div name="second_row" data-loaded="1">
[%- PROCESS order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
</div>
[%- ELSE %]
<div name="second_row" id="second_row_[% ID %]">
[%- LxERP.t8("Loading...") %]
</div>
[%- END %]
</td>
</tr>
</tbody>
templates/webpages/invoice/tabs/_second_row.html
[%- USE T8 %]
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
[%- USE P %]
<table>
<tr><td colspan="100%">
[%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "purchase_order") %]
<b>[%- 'Serial No.' | $T8 %]</b>&nbsp;
[%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15 "data-validate"="trimmed_whitespaces") %]&nbsp;
[%- END %]
<b>[%- 'Project' | $T8 %]</b>&nbsp;
[% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %]&nbsp;
[%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "purchase_order") %]
<b>[%- 'Reqdate' | $T8 %]</b>&nbsp;
[% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %]&nbsp;
[%- END %]
<b>[%- 'Subtotal' | $T8 %]</b>&nbsp;
[% L.yes_no_tag("order.orderitems[].subtotal", ITEM.subtotal) %]&nbsp;
[%- IF TYPE == "sales_order" %]
<b>[%- 'Recurring billing' | $T8 %]</b>&nbsp;
[% 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;
[%- END %]
[%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "sales_quotation") %]
<b>[%- 'Ertrag' | $T8 %]</b>&nbsp;
<span name="linemargin">
<span[%- IF ITEM.marge_total < 0 -%] class="plus0"[%- END -%]>
[%- LxERP.format_amount(ITEM.marge_total, 2, 0) %]&nbsp;&nbsp;
[%- LxERP.format_amount(ITEM.marge_percent, 2, 0) %]%
</span>
</span>&nbsp;
<b>[%- 'LP' | $T8 %]</b>&nbsp;
[%- LxERP.format_amount(ITEM.part.listprice, 2, 0) %]&nbsp;
<b>[%- 'EK' | $T8 %]</b>&nbsp;
[%- L.input_tag("order.orderitems[].lastcost_as_number",
ITEM.lastcost_as_number,
size = 5,
class="recalc reformat_number numeric") %]&nbsp;
[%- END %]
<b>[%- 'On Hand' | $T8 %]</b>&nbsp;
<span class="numeric[%- IF ITEM.part.onhand < ITEM.part.rop -%] plus0[%- END -%]">
[%- ITEM.part.onhand_as_number -%]&nbsp;[%- ITEM.part.unit -%]
</span>&nbsp;
<b>[%- 'Optional' | $T8 %]</b>&nbsp;
[%- L.yes_no_tag("order.orderitems[].optional", ITEM.optional
class="recalc") %]&nbsp;
</td></tr>
<tr>
[%- SET n = 0 %]
[%- FOREACH var = ITEM.cvars_by_config %]
[%- NEXT UNLESS (var.config.processed_flags.editable && ITEM.part.cvar_by_name(var.config.name).is_valid) %]
[%- SET n = n + 1 %]
<th>
[% var.config.description %]
</th>
<td>
[% L.hidden_tag('order.orderitems[].custom_variables[+].config_id', var.config.id) %]
[% L.hidden_tag('order.orderitems[].custom_variables[].id', var.id) %]
[% L.hidden_tag('order.orderitems[].custom_variables[].sub_module', var.sub_module) %]
[% INCLUDE 'common/render_cvar_input.html' var_name='order.orderitems[].custom_variables[].unparsed_value' %]
</td>
[%- IF (n % (MYCONFIG.form_cvars_nr_cols || 3)) == 0 %]
</tr><tr>
[%- END %]
[%- END %]
</tr>
</table>
templates/webpages/invoice/tabs/_tax_row.html
[%- USE T8 %]
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
<tr class="tax_row">
<th align="right">[%- IF TAXINCLUDED %][%- 'Including' | $T8 %]&nbsp;[%- END %][%- TAX.tax.taxdescription %] [% TAX.tax.rate_as_percent %]%</th>
<td align="right">[%- LxERP.format_amount(TAX.amount, 2, 0) %]</td>
</tr>
[%- IF TAXINCLUDED %]
<tr class="tax_row">
<th align="right">[%- 'Net amount' | $T8 %]</th>
<td align="right">[%- LxERP.format_amount(TAX.netamount, 2, 0) %]</td>
</tr>
[%- END%]
templates/webpages/invoice/tabs/basic_data.html
[%- USE T8 %]
[%- USE HTML %]
[%- USE LxERP %]
[%- USE L %]
[%- USE P %]
[%- INCLUDE 'generic/set_longdescription.html' %]
<div id="ui-tabs-basic-data">
<table width="100%">
<tr valign="top">
<td>
<table width="100%">
<tr>
<th align="right">[%- SELF.cv == "customer" ? LxERP.t8('Customer') : LxERP.t8('Vendor') -%]</th>
[% SET cv_id = SELF.cv _ '_id' %]
<td>
[% P.customer_vendor.picker("invoice.${SELF.cv}" _ '_id', SELF.invoice.$cv_id, type=SELF.cv, show_details="1",
style='width: 300px') %]
</td>
</tr>
<tr id='cp_row' [%- IF !SELF.invoice.${SELF.cv}.contacts.size %]style='display:none'[%- END %]>
<th align="right">[% 'Contact Person' | $T8 %]</th>
<td>[% L.select_tag('invoice.cp_id',
SELF.invoice.${SELF.cv}.contacts,
default=SELF.invoice.cp_id,
title_key='full_name_dep',
value_key='cp_id',
with_empty=1,
style='width: 300px') %]</td>
</tr>
<tr>
<th align="right">[% 'Shipping Address' | $T8 %]</th>
<td>
<span id='shipto_selection' [%- IF !SELF.invoice.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
[% shiptos = [ { shipto_id => "", displayable_id => LxERP.t8("No/individual shipping address") } ] ;
FOREACH s = SELF.invoice.${SELF.cv}.shipto ;
shiptos.push(s) ;
END ;
L.select_tag('invoice.shipto_id',
shiptos,
default=SELF.invoice.shipto_id,
title_key='displayable_id',
value_key='shipto_id',
with_empty=0,
style='width: 300px') %]
</span>
[% L.button_tag("kivi.invoice.edit_custom_shipto()", LxERP.t8("Custom shipto")) %]
</td>
</tr>
[%- IF SELF.cv == "customer" %]
<tr id="billing_address_row"[% IF !SELF.invoice.customer.additional_billing_addresses.as_list.size %] style="display:none"[% END %]>
<th align="right">[% 'Custom Billing Address' | $T8 %]</th>
<td>
[% L.select_tag('invoice.billing_address_id',
SELF.invoice.customer.additional_billing_addresses,
default=SELF.invoice.billing_address_id,
title_key='displayable_id',
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff