Revision f49e9deb
Von Sven Schöling vor 9 Monaten hinzugefügt
- ID f49e9deb77039f7cac10d23ab93ebb9cd66b8870
- Vorgänger b0c61725
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'>•</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'>•</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> |
|
11 |
[%- L.input_tag("order.orderitems[].serialnumber", ITEM.serialnumber, size = 15 "data-validate"="trimmed_whitespaces") %] |
|
12 |
[%- END %] |
|
13 |
<b>[%- 'Project' | $T8 %]</b> |
|
14 |
[% P.project.picker("order.orderitems[].project_id", ITEM.project_id, size = 15) %] |
|
15 |
[%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "purchase_order") %] |
|
16 |
<b>[%- 'Reqdate' | $T8 %]</b> |
|
17 |
[% L.date_tag("order.orderitems[].reqdate_as_date", ITEM.reqdate_as_date) %] |
|
18 |
[%- END %] |
|
19 |
<b>[%- 'Subtotal' | $T8 %]</b> |
|
20 |
[% L.yes_no_tag("order.orderitems[].subtotal", ITEM.subtotal) %] |
|
21 |
[%- IF TYPE == "sales_order" %] |
|
22 |
<b>[%- 'Recurring billing' | $T8 %]</b> |
|
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) %] |
|
24 |
[%- END %] |
|
25 |
[%- IF (TYPE == "sales_order_intake" || TYPE == "sales_order" || TYPE == "sales_quotation") %] |
|
26 |
<b>[%- 'Ertrag' | $T8 %]</b> |
|
27 |
<span name="linemargin"> |
|
28 |
<span[%- IF ITEM.marge_total < 0 -%] class="plus0"[%- END -%]> |
|
29 |
[%- LxERP.format_amount(ITEM.marge_total, 2, 0) %] |
|
30 |
[%- LxERP.format_amount(ITEM.marge_percent, 2, 0) %]% |
|
31 |
</span> |
|
32 |
</span> |
|
33 |
<b>[%- 'LP' | $T8 %]</b> |
|
34 |
[%- LxERP.format_amount(ITEM.part.listprice, 2, 0) %] |
|
35 |
<b>[%- 'EK' | $T8 %]</b> |
|
36 |
[%- L.input_tag("order.orderitems[].lastcost_as_number", |
|
37 |
ITEM.lastcost_as_number, |
|
38 |
size = 5, |
|
39 |
class="recalc reformat_number numeric") %] |
|
40 |
[%- END %] |
|
41 |
<b>[%- 'On Hand' | $T8 %]</b> |
|
42 |
<span class="numeric[%- IF ITEM.part.onhand < ITEM.part.rop -%] plus0[%- END -%]"> |
|
43 |
[%- ITEM.part.onhand_as_number -%] [%- ITEM.part.unit -%] |
|
44 |
</span> |
|
45 |
<b>[%- 'Optional' | $T8 %]</b> |
|
46 |
[%- L.yes_no_tag("order.orderitems[].optional", ITEM.optional |
|
47 |
class="recalc") %] |
|
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 %] [%- 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', |
Auch abrufbar als: Unified diff
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>{order}- 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
> $::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)