From: Moritz Bunkus Date: Thu, 10 Dec 2020 14:58:08 +0000 (+0100) Subject: Merge branch 'f-factur-x-und-xrechnung' X-Git-Tag: kivitendo-mebil_0.1-0~9^2~562 X-Git-Url: http://wagnertech.de/gitweb/gitweb.cgi/mfinanz.git/commitdiff_plain/4dc48e117cfdb6e59c2d8b9d8087ef8a3bc245a4?hp=e2f0105f947c01d3d45be41833f3a7e39eef2f7b Merge branch 'f-factur-x-und-xrechnung' --- diff --git a/.mailmap b/.mailmap index bf5a206aa..bfb19e55b 100644 --- a/.mailmap +++ b/.mailmap @@ -1,7 +1,9 @@ +Bernd Bleßmann Bernd Bleßmann Bernd Bleßmann bernd Christian Wittmer ChrisWi Geoffrey Richardson +Geoffrey Richardson G. Richardson Geoffrey Richardson Geoffrey Richardson Geoffrey Richardson grichardson @@ -9,22 +11,29 @@ Holger Lindemann Holger Lindemann Jan Büren Jan Büren +Jan Büren +Jan Büren Jan Büren Jan Büren Jan Büren Jan Büren Jan Büren Joachim Zach +Marei Peischl Marei (peiTeX) +Marei Peischl Marei Peischl (peiTeX) Martin Helmling Martin Helmling Martin Helmling Martin Helmling martin.helmling@octosoft.eu Martin Helmling Martin Helmling mh@waldpark.octosoft.eu -Moritz Bunkus +Moritz Bunkus +Moritz Bunkus Niclas Zimmermann +Rolf Eike Beer Rolf Eike Beer Roman Karuschka R. Karuschka Roman Karuschka Roman Karushka Roman Karuschka roman -Sven Schöling +Sven Schöling +Sven Schöling Timo Eickmeyer T. Eickmeyer Waldemar Toews Wulf Coulmann Wulf diff --git a/SL/AP.pm b/SL/AP.pm index 937af8b58..71c2b4b00 100644 --- a/SL/AP.pm +++ b/SL/AP.pm @@ -139,14 +139,14 @@ sub _post_transaction { $query = qq|UPDATE ap SET invnumber = ?, transdate = ?, ordnumber = ?, vendor_id = ?, taxincluded = ?, - amount = ?, duedate = ?, deliverydate = ?, paid = ?, netamount = ?, + amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?, netamount = ?, currency_id = (SELECT id FROM currencies WHERE name = ?), notes = ?, department_id = ?, storno = ?, storno_id = ?, globalproject_id = ?, direct_debit = ? WHERE id = ?|; @values = ($form->{invnumber}, conv_date($form->{transdate}), $form->{ordnumber}, conv_i($form->{vendor_id}), $form->{taxincluded} ? 't' : 'f', $form->{invtotal}, - conv_date($form->{duedate}), conv_date($form->{deliverydate}), + conv_date($form->{duedate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}), $form->{invpaid}, $form->{netamount}, $form->{currency}, $form->{notes}, conv_i($form->{department_id}), $form->{storno}, diff --git a/SL/AR.pm b/SL/AR.pm index 3d31a61a4..b856f44a7 100644 --- a/SL/AR.pm +++ b/SL/AR.pm @@ -134,14 +134,14 @@ sub _post_transaction { $query = qq|UPDATE ar set invnumber = ?, ordnumber = ?, transdate = ?, customer_id = ?, - taxincluded = ?, amount = ?, duedate = ?, deliverydate = ?, paid = ?, + taxincluded = ?, amount = ?, duedate = ?, deliverydate = ?, tax_point = ?, paid = ?, currency_id = (SELECT id FROM currencies WHERE name = ?), netamount = ?, notes = ?, department_id = ?, employee_id = ?, storno = ?, storno_id = ?, globalproject_id = ?, direct_debit = ? WHERE id = ?|; my @values = ($form->{invnumber}, $form->{ordnumber}, conv_date($form->{transdate}), conv_i($form->{customer_id}), $form->{taxincluded} ? 't' : 'f', $form->{amount}, - conv_date($form->{duedate}), conv_date($form->{deliverydate}), $form->{paid}, + conv_date($form->{duedate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}), $form->{paid}, $form->{currency}, $form->{netamount}, $form->{notes}, conv_i($form->{department_id}), conv_i($form->{employee_id}), $form->{storno} ? 't' : 'f', $form->{storno_id}, diff --git a/SL/BackgroundJob/CreatePeriodicInvoices.pm b/SL/BackgroundJob/CreatePeriodicInvoices.pm index 52415f20e..f7b9100ad 100644 --- a/SL/BackgroundJob/CreatePeriodicInvoices.pm +++ b/SL/BackgroundJob/CreatePeriodicInvoices.pm @@ -224,10 +224,17 @@ sub _create_periodic_invoice { $invoice = SL::DB::Invoice->new_from($order); + my $tax_point = ($invoice->tax_point // $time_period_vars->{period_end_date}->[0])->clone; + + while ($tax_point < $period_start_date) { + $tax_point->add(months => $config->get_billing_period_length); + } + my $intnotes = $invoice->intnotes ? $invoice->intnotes . "\n\n" : ''; $intnotes .= "Automatisch am " . $invdate->to_lxoffice . " erzeugte Rechnung"; $invoice->assign_attributes(deliverydate => $period_start_date, + tax_point => $tax_point, intnotes => $intnotes, employee => $order->employee, # new_from sets employee to import user direct_debit => $config->direct_debit, diff --git a/SL/Controller/BankTransaction.pm b/SL/Controller/BankTransaction.pm index dc66ef090..e7137bb38 100644 --- a/SL/Controller/BankTransaction.pm +++ b/SL/Controller/BankTransaction.pm @@ -206,13 +206,10 @@ sub gather_bank_transactions_and_proposals { # to qualify as a proposal there has to be # * agreement >= 5 TODO: make threshold configurable in configuration # * there must be only one exact match - # * depending on whether sales or purchase the amount has to have the correct sign (so Gutschriften don't work?) my $proposal_threshold = 5; my @otherproposals = grep { ($_->{agreement} >= $proposal_threshold) && (1 == scalar @{ $_->{proposals} }) - && (@{ $_->{proposals} }[0]->is_sales ? abs(@{ $_->{proposals} }[0]->amount - $_->amount) < 0.01 - : abs(@{ $_->{proposals} }[0]->amount + $_->amount) < 0.01) } @{ $bank_transactions }; push @proposals, @otherproposals; diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm index 29ff0ac49..b51802f62 100644 --- a/SL/Controller/Order.pm +++ b/SL/Controller/Order.pm @@ -581,27 +581,10 @@ sub action_get_has_active_periodic_invoices { sub action_save_and_delivery_order { my ($self) = @_; - my $errors = $self->save(); - - if (scalar @{ $errors }) { - $self->js->flash('error', $_) foreach @{ $errors }; - return $self->js->render(); - } - - my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved') - : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved') - : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved') - : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved') - : ''; - flash_later('info', $text); - - my @redirect_params = ( + $self->save_and_redirect_to( controller => 'oe.pl', action => 'oe_delivery_order_from_order', - id => $self->order->id, ); - - $self->redirect_to(@redirect_params); } # save the order and redirect to the frontend subroutine for a new @@ -609,27 +592,10 @@ sub action_save_and_delivery_order { sub action_save_and_invoice { my ($self) = @_; - my $errors = $self->save(); - - if (scalar @{ $errors }) { - $self->js->flash('error', $_) foreach @{ $errors }; - return $self->js->render(); - } - - my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved') - : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved') - : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved') - : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved') - : ''; - flash_later('info', $text); - - my @redirect_params = ( + $self->save_and_redirect_to( controller => 'oe.pl', action => 'oe_invoice_from_order', - id => $self->order->id, ); - - $self->redirect_to(@redirect_params); } # workflow from sales quotation to sales order @@ -646,27 +612,10 @@ sub action_purchase_order { sub action_save_and_ap_transaction { my ($self) = @_; - my $errors = $self->save(); - - if (scalar @{ $errors }) { - $self->js->flash('error', $_) foreach @{ $errors }; - return $self->js->render(); - } - - my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved') - : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved') - : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved') - : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved') - : ''; - flash_later('info', $text); - - my @redirect_params = ( + $self->save_and_redirect_to( controller => 'ap.pl', action => 'add_from_purchase_order', - id => $self->order->id, ); - - $self->redirect_to(@redirect_params); } # set form elements in respect to a changed customer or vendor @@ -2111,6 +2060,26 @@ sub nr_key { : ''; } +sub save_and_redirect_to { + my ($self, %params) = @_; + + my $errors = $self->save(); + + if (scalar @{ $errors }) { + $self->js->flash('error', $_) foreach @{ $errors }; + return $self->js->render(); + } + + my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved') + : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved') + : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved') + : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved') + : ''; + flash_later('info', $text); + + $self->redirect_to(%params, id => $self->order->id); +} + 1; __END__ diff --git a/SL/DATEV.pm b/SL/DATEV.pm index af188f70a..c6ce5b2a0 100644 --- a/SL/DATEV.pm +++ b/SL/DATEV.pm @@ -517,7 +517,7 @@ sub generate_datev_data { my $query = qq|SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ar.id, ac.amount, ac.taxkey, ac.memo, - ar.invnumber, ar.duedate, ar.amount as umsatz, ar.deliverydate, ar.itime::date, + ar.invnumber, ar.duedate, ar.amount as umsatz, COALESCE(ar.tax_point, ar.deliverydate) AS deliverydate, ar.itime::date, ct.name, ct.ustid, ct.customernumber AS vcnumber, ct.id AS customer_id, NULL AS vendor_id, $ar_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link, ar.invoice, @@ -546,7 +546,7 @@ sub generate_datev_data { UNION ALL SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,ap.id, ac.amount, ac.taxkey, ac.memo, - ap.invnumber, ap.duedate, ap.amount as umsatz, ap.deliverydate, ap.itime::date, + ap.invnumber, ap.duedate, ap.amount as umsatz, COALESCE(ap.tax_point, ap.deliverydate) AS deliverydate, ap.itime::date, ct.name, ct.ustid, ct.vendornumber AS vcnumber, NULL AS customer_id, ct.id AS vendor_id, $ap_accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link, ap.invoice, @@ -575,7 +575,7 @@ sub generate_datev_data { UNION ALL SELECT ac.acc_trans_id, ac.transdate, ac.gldate, ac.trans_id,gl.id, ac.amount, ac.taxkey, ac.memo, - gl.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, gl.deliverydate, gl.itime::date, + gl.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, COALESCE(gl.tax_point, gl.deliverydate) AS deliverydate, gl.itime::date, gl.description AS name, NULL as ustid, '' AS vcname, NULL AS customer_id, NULL AS vendor_id, c.accno, c.description AS accname, c.taxkey_id as charttax, c.datevautomatik, c.id, ac.chart_link AS link, FALSE AS invoice, diff --git a/SL/DB/BankTransaction.pm b/SL/DB/BankTransaction.pm index e2ddede4e..c02ebb93e 100644 --- a/SL/DB/BankTransaction.pm +++ b/SL/DB/BankTransaction.pm @@ -79,7 +79,7 @@ sub get_agreement_with_invoice { payment_within_30_days => 1, remote_account_number => 3, skonto_exact_amount => 5, - wrong_sign => -1, + wrong_sign => -4, sepa_export_item => 5, batch_sepa_transaction => 20, ); @@ -162,11 +162,13 @@ sub get_agreement_with_invoice { } #check sign - if ( $invoice->is_sales && $self->amount < 0 ) { + if (( $invoice->is_sales && $invoice->amount > 0 && $self->amount < 0 ) || + ( $invoice->is_sales && $invoice->amount < 0 && $self->amount > 0 ) ) { # sales credit note $agreement += $points{wrong_sign}; $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') '; } - if ( ! $invoice->is_sales && $self->amount > 0 ) { + if (( !$invoice->is_sales && $invoice->amount > 0 && $self->amount > 0) || + ( !$invoice->is_sales && $invoice->amount < 0 && $self->amount < 0) ) { # purchase credit note $agreement += $points{wrong_sign}; $rule_matches .= 'wrong_sign(' . $points{'wrong_sign'} . ') '; } diff --git a/SL/DB/Helper/FlattenToForm.pm b/SL/DB/Helper/FlattenToForm.pm index 85591c07d..ccd6ca045 100644 --- a/SL/DB/Helper/FlattenToForm.pm +++ b/SL/DB/Helper/FlattenToForm.pm @@ -15,7 +15,7 @@ sub flatten_to_form { _copy($self, $form, '', '', 0, qw(id type taxzone_id ordnumber quonumber invnumber donumber cusordnumber taxincluded shippingpoint shipvia notes intnotes cp_id employee_id salesman_id closed department_id language_id payment_id delivery_customer_id delivery_vendor_id shipto_id proforma globalproject_id delivered transaction_description container_type accepted_by_customer invoice storno storno_id dunning_config_id - orddate quodate reqdate gldate duedate deliverydate datepaid transdate delivery_term_id)); + orddate quodate reqdate gldate duedate deliverydate datepaid transdate tax_point delivery_term_id)); $form->{currency} = $form->{curr} = $self->currency_id ? $self->currency->name || '' : ''; if ( $vc eq 'customer' ) { diff --git a/SL/DB/Helper/PriceTaxCalculator.pm b/SL/DB/Helper/PriceTaxCalculator.pm index f5bc61c4a..15850d558 100644 --- a/SL/DB/Helper/PriceTaxCalculator.pm +++ b/SL/DB/Helper/PriceTaxCalculator.pm @@ -54,7 +54,7 @@ sub calculate_prices_and_taxes { $self->netamount( 0); $self->marge_total(0); - SL::DB::Manager::Chart->cache_taxkeys(date => $self->deliverydate // $self->transdate); + SL::DB::Manager::Chart->cache_taxkeys(date => $self->effective_tax_point); my $idx = 0; foreach my $item (@{ $self->items_sorted }) { @@ -110,7 +110,7 @@ sub _calculate_item { $data->{invoicediff} += $sellprice * (1 - $item->discount) * $item->qty * $data->{exchangerate} / $item->price_factor - $linetotal if $self->taxincluded; - my $taxkey = $part->get_taxkey(date => $self->deliverydate // $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id); + my $taxkey = $part->get_taxkey(date => $self->effective_tax_point, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id); my $tax_rate = $taxkey->tax->rate; my $tax_amount = undef; diff --git a/SL/DB/Invoice.pm b/SL/DB/Invoice.pm index 93c17e4ec..26faca1a3 100644 --- a/SL/DB/Invoice.pm +++ b/SL/DB/Invoice.pm @@ -156,7 +156,7 @@ sub new_from { my (@columns, @item_columns, $item_parent_id_column, $item_parent_column); if (ref($source) eq 'SL::DB::Order') { - @columns = qw(quonumber delivery_customer_id delivery_vendor_id); + @columns = qw(quonumber delivery_customer_id delivery_vendor_id tax_point); @item_columns = qw(subtotal); $item_parent_id_column = 'trans_id'; @@ -173,7 +173,7 @@ sub new_from { $terms = $source->customer->payment_terms if !defined $terms && $source->customer; my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id - cp_id language_id taxzone_id globalproject_id transaction_description currency_id delivery_term_id), @columns), + cp_id language_id taxzone_id tax_point globalproject_id transaction_description currency_id delivery_term_id), @columns), transdate => $params{transdate} // DateTime->today_local, gldate => DateTime->today_local, duedate => $terms ? $terms->calc_date(reference_date => DateTime->today_local) : DateTime->today_local, @@ -609,6 +609,12 @@ sub mark_as_paid { $self->update_attributes(paid => $self->amount); } +sub effective_tax_point { + my ($self) = @_; + + return $self->tax_point || $self->deliverydate || $self->transdate; +} + 1; __END__ diff --git a/SL/DB/MetaSetup/DeliveryOrder.pm b/SL/DB/MetaSetup/DeliveryOrder.pm index d242fcd3c..780f4318c 100644 --- a/SL/DB/MetaSetup/DeliveryOrder.pm +++ b/SL/DB/MetaSetup/DeliveryOrder.pm @@ -35,6 +35,7 @@ __PACKAGE__->meta->columns( shippingpoint => { type => 'text' }, shipto_id => { type => 'integer' }, shipvia => { type => 'text' }, + tax_point => { type => 'date' }, taxincluded => { type => 'boolean' }, taxzone_id => { type => 'integer', not_null => 1 }, transaction_description => { type => 'text' }, diff --git a/SL/DB/MetaSetup/GLTransaction.pm b/SL/DB/MetaSetup/GLTransaction.pm index f59251362..4d0e41b13 100644 --- a/SL/DB/MetaSetup/GLTransaction.pm +++ b/SL/DB/MetaSetup/GLTransaction.pm @@ -23,6 +23,7 @@ __PACKAGE__->meta->columns( reference => { type => 'text' }, storno => { type => 'boolean', default => 'false' }, storno_id => { type => 'integer' }, + tax_point => { type => 'date' }, taxincluded => { type => 'boolean' }, transdate => { type => 'date', default => 'now' }, type => { type => 'text' }, diff --git a/SL/DB/MetaSetup/Invoice.pm b/SL/DB/MetaSetup/Invoice.pm index b88c2f57c..9b10c938c 100644 --- a/SL/DB/MetaSetup/Invoice.pm +++ b/SL/DB/MetaSetup/Invoice.pm @@ -51,6 +51,7 @@ __PACKAGE__->meta->columns( shipvia => { type => 'text' }, storno => { type => 'boolean', default => 'false' }, storno_id => { type => 'integer' }, + tax_point => { type => 'date' }, taxincluded => { type => 'boolean' }, taxzone_id => { type => 'integer', not_null => 1 }, transaction_description => { type => 'text' }, diff --git a/SL/DB/MetaSetup/Order.pm b/SL/DB/MetaSetup/Order.pm index d6922bd53..6e707ae65 100644 --- a/SL/DB/MetaSetup/Order.pm +++ b/SL/DB/MetaSetup/Order.pm @@ -44,6 +44,7 @@ __PACKAGE__->meta->columns( shippingpoint => { type => 'text' }, shipto_id => { type => 'integer' }, shipvia => { type => 'text' }, + tax_point => { type => 'date' }, taxincluded => { type => 'boolean' }, taxzone_id => { type => 'integer', not_null => 1 }, transaction_description => { type => 'text' }, diff --git a/SL/DB/MetaSetup/PurchaseInvoice.pm b/SL/DB/MetaSetup/PurchaseInvoice.pm index c9c74f0d4..4a443ac61 100644 --- a/SL/DB/MetaSetup/PurchaseInvoice.pm +++ b/SL/DB/MetaSetup/PurchaseInvoice.pm @@ -39,6 +39,7 @@ __PACKAGE__->meta->columns( shipvia => { type => 'text' }, storno => { type => 'boolean', default => 'false' }, storno_id => { type => 'integer' }, + tax_point => { type => 'date' }, taxincluded => { type => 'boolean', default => 'false' }, taxzone_id => { type => 'integer', not_null => 1 }, transaction_description => { type => 'text' }, diff --git a/SL/DB/MetaSetup/RecordTemplate.pm b/SL/DB/MetaSetup/RecordTemplate.pm index a9f6f8814..83850fffa 100644 --- a/SL/DB/MetaSetup/RecordTemplate.pm +++ b/SL/DB/MetaSetup/RecordTemplate.pm @@ -23,6 +23,7 @@ __PACKAGE__->meta->columns( notes => { type => 'text' }, ob_transaction => { type => 'boolean', default => 'false', not_null => 1 }, ordnumber => { type => 'text' }, + payment_id => { type => 'integer' }, project_id => { type => 'integer' }, reference => { type => 'text' }, show_details => { type => 'boolean', default => 'false', not_null => 1 }, @@ -62,6 +63,11 @@ __PACKAGE__->meta->foreign_keys( key_columns => { employee_id => 'id' }, }, + payment => { + class => 'SL::DB::PaymentTerm', + key_columns => { payment_id => 'id' }, + }, + project => { class => 'SL::DB::Project', key_columns => { project_id => 'id' }, diff --git a/SL/DB/Order.pm b/SL/DB/Order.pm index d996d13b6..8228def98 100644 --- a/SL/DB/Order.pm +++ b/SL/DB/Order.pm @@ -139,9 +139,17 @@ sub is_type { } sub deliverydate { - # oe doesn't have deliverydate, but PTC checks for deliverydate or transdate to determine tax - # oe can't deal with deviating tax rates, but at least make sure PTC doesn't barf - return shift->transdate; + # oe doesn't have deliverydate, but it does have reqdate. + # But this has a different meaning for sales quotations. + # deliverydate can be used to determine tax if tax_point isn't set. + + return $_[0]->reqdate if $_[0]->type ne 'sales_quotation'; +} + +sub effective_tax_point { + my ($self) = @_; + + return $self->tax_point || $self->deliverydate || $self->transdate; } sub displayable_type { @@ -322,7 +330,7 @@ sub new_from { my %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id department_id employee_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes - ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id + ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded tax_point taxzone_id transaction_description vendor_id )), quotation => !!($destination_type =~ m{quotation$}), @@ -429,7 +437,7 @@ sub new_from_multi { # set this entries to undef that yield different information my %attributes; - foreach my $attr (qw(ordnumber transdate reqdate taxincluded shippingpoint + foreach my $attr (qw(ordnumber transdate reqdate tax_point taxincluded shippingpoint shipvia notes closed delivered reqdate quonumber cusordnumber proforma transaction_description order_probability expected_billing_date)) { diff --git a/SL/DB/PurchaseInvoice.pm b/SL/DB/PurchaseInvoice.pm index a50884cb1..dd49433c6 100644 --- a/SL/DB/PurchaseInvoice.pm +++ b/SL/DB/PurchaseInvoice.pm @@ -214,6 +214,12 @@ sub mark_as_paid { $self->update_attributes(paid => $self->amount); } +sub effective_tax_point { + my ($self) = @_; + + return $self->tax_point || $self->deliverydate || $self->transdate; +} + 1; diff --git a/SL/DO.pm b/SL/DO.pm index 034189fc3..90599a7de 100644 --- a/SL/DO.pm +++ b/SL/DO.pm @@ -511,7 +511,7 @@ SQL $query = qq|UPDATE delivery_orders SET donumber = ?, ordnumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?, - customer_id = ?, reqdate = ?, + customer_id = ?, reqdate = ?, tax_point = ?, shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, closed = ?, delivered = ?, department_id = ?, language_id = ?, shipto_id = ?, globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, @@ -522,7 +522,7 @@ SQL @values = ($form->{donumber}, $form->{ordnumber}, $form->{cusordnumber}, conv_date($form->{transdate}), conv_i($form->{vendor_id}), conv_i($form->{customer_id}), - conv_date($form->{reqdate}), $form->{shippingpoint}, $form->{shipvia}, + conv_date($form->{reqdate}), conv_date($form->{tax_point}), $form->{shippingpoint}, $form->{shipvia}, $restricter->process($form->{notes}), $form->{intnotes}, $form->{closed} ? 't' : 'f', $form->{delivered} ? "t" : "f", conv_i($form->{department_id}), conv_i($form->{language_id}), conv_i($form->{shipto_id}), @@ -693,7 +693,7 @@ sub retrieve { # so if any of these infos is important (or even different) for any item, # it will be killed out and then has to be fetched from the item scope query further down $query = - qq|SELECT dord.cp_id, dord.donumber, dord.ordnumber, dord.transdate, dord.reqdate, + qq|SELECT dord.cp_id, dord.donumber, dord.ordnumber, dord.transdate, dord.reqdate, dord.tax_point, dord.shippingpoint, dord.shipvia, dord.notes, dord.intnotes, e.name AS employee, dord.employee_id, dord.salesman_id, dord.${vc}_id, cv.name AS ${vc}, diff --git a/SL/Form.pm b/SL/Form.pm index 92d32e76f..c92d267a5 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -382,10 +382,11 @@ sub create_http_response { my $session_cookie_value = $main::auth->get_session_id(); if ($session_cookie_value) { - $session_cookie = $cgi->cookie('-name' => $main::auth->get_session_cookie_name(), - '-value' => $session_cookie_value, - '-path' => $uri->path, - '-secure' => $::request->is_https); + $session_cookie = $cgi->cookie('-name' => $main::auth->get_session_cookie_name(), + '-value' => $session_cookie_value, + '-path' => $uri->path, + '-expires' => '+' . $::auth->{session_timeout} . 'm', + '-secure' => $::request->is_https); } } @@ -2566,7 +2567,7 @@ sub create_links { $query = qq|SELECT a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid, a.deliverydate, - a.duedate, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes, + a.duedate, a.tax_point, a.ordnumber, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.notes, a.mtime, a.itime, a.intnotes, a.department_id, a.amount AS oldinvtotal, a.paid AS oldtotalpaid, a.employee_id, a.gldate, a.type, @@ -3229,7 +3230,7 @@ sub prepare_for_printing { # Format dates. $self->format_dates($output_dateformat, $output_longdates, - qw(invdate orddate quodate pldate duedate reqdate transdate shippingdate deliverydate validitydate paymentdate datepaid + qw(invdate orddate quodate pldate duedate reqdate transdate tax_point shippingdate deliverydate validitydate paymentdate datepaid transdate_oe deliverydate_oe employee_startdate employee_enddate), grep({ /^(?:datepaid|transdate_oe|reqdate|deliverydate|deliverydate_oe|transdate)_\d+$/ } keys(%{$self}))); diff --git a/SL/GL.pm b/SL/GL.pm index 245e5b800..9eb741d83 100644 --- a/SL/GL.pm +++ b/SL/GL.pm @@ -123,12 +123,12 @@ sub _post_transaction { $query = qq|UPDATE gl SET reference = ?, description = ?, notes = ?, - transdate = ?, deliverydate = ?, department_id = ?, taxincluded = ?, + transdate = ?, deliverydate = ?, tax_point = ?, department_id = ?, taxincluded = ?, storno = ?, storno_id = ?, ob_transaction = ?, cb_transaction = ? WHERE id = ?|; @values = ($form->{reference}, $form->{description}, $form->{notes}, - conv_date($form->{transdate}), conv_date($form->{deliverydate}), conv_i($form->{department_id}), $form->{taxincluded} ? 't' : 'f', + conv_date($form->{transdate}), conv_date($form->{deliverydate}), conv_date($form->{tax_point}), conv_i($form->{department_id}), $form->{taxincluded} ? 't' : 'f', $form->{storno} ? 't' : 'f', conv_i($form->{storno_id}), $form->{ob_transaction} ? 't' : 'f', $form->{cb_transaction} ? 't' : 'f', conv_i($form->{id})); do_query($form, $dbh, $query, @values); @@ -637,7 +637,7 @@ sub transaction { if ($form->{id}) { $query = - qq|SELECT g.reference, g.description, g.notes, g.transdate, g.deliverydate, + qq|SELECT g.reference, g.description, g.notes, g.transdate, g.deliverydate, g.tax_point, g.storno, g.storno_id, g.department_id, d.description AS department, e.name AS employee, g.taxincluded, g.gldate, diff --git a/SL/Helper/Inventory.pm b/SL/Helper/Inventory.pm new file mode 100644 index 000000000..da3e5b1d8 --- /dev/null +++ b/SL/Helper/Inventory.pm @@ -0,0 +1,779 @@ +package SL::Helper::Inventory; + +use strict; +use Carp; +use DateTime; +use Exporter qw(import); +use List::Util qw(min sum); +use List::UtilsBy qw(sort_by); +use List::MoreUtils qw(any); +use POSIX qw(ceil); + +use SL::Locale::String qw(t8); +use SL::MoreCommon qw(listify); +use SL::DBUtils qw(selectall_hashref_query selectrow_query); +use SL::DB::TransferType; +use SL::Helper::Number qw(_format_number _round_number); +use SL::Helper::Inventory::Allocation; +use SL::X; + +our @EXPORT_OK = qw(get_stock get_onhand allocate allocate_for_assembly produce_assembly check_constraints); +our %EXPORT_TAGS = (ALL => \@EXPORT_OK); + +sub _get_stock_onhand { + my (%params) = @_; + + my $onhand_mode = !!$params{onhand}; + + my @selects = ( + 'SUM(qty) AS qty', + 'MIN(EXTRACT(epoch FROM inventory.itime)) AS itime', + ); + my @values; + my @where; + my @groups; + + if ($params{part}) { + my @ids = map { ref $_ ? $_->id : $_ } listify($params{part}); + push @where, sprintf "parts_id IN (%s)", join ', ', ("?") x @ids; + push @values, @ids; + } + + if ($params{bin}) { + my @ids = map { ref $_ ? $_->id : $_ } listify($params{bin}); + push @where, sprintf "bin_id IN (%s)", join ', ', ("?") x @ids; + push @values, @ids; + } + + if ($params{warehouse}) { + my @ids = map { ref $_ ? $_->id : $_ } listify($params{warehouse}); + push @where, sprintf "warehouse.id IN (%s)", join ', ', ("?") x @ids; + push @values, @ids; + } + + if ($params{chargenumber}) { + my @ids = listify($params{chargenumber}); + push @where, sprintf "chargenumber IN (%s)", join ', ', ("?") x @ids; + push @values, @ids; + } + + if ($params{date}) { + Carp::croak("not DateTime ".$params{date}) unless ref($params{date}) eq 'DateTime'; + push @where, sprintf "shippingdate <= ?"; + push @values, $params{date}; + } + + if (!$params{bestbefore} && $onhand_mode && default_show_bestbefore()) { + $params{bestbefore} = DateTime->now_local; + } + + if ($params{bestbefore}) { + Carp::croak("not DateTime ".$params{date}) unless ref($params{bestbefore}) eq 'DateTime'; + push @where, sprintf "(bestbefore IS NULL OR bestbefore >= ?)"; + push @values, $params{bestbefore}; + } + + # by + my %allowed_by = ( + part => [ qw(parts_id) ], + bin => [ qw(bin_id inventory.warehouse_id)], + warehouse => [ qw(inventory.warehouse_id) ], + chargenumber => [ qw(chargenumber) ], + bestbefore => [ qw(bestbefore) ], + for_allocate => [ qw(parts_id bin_id inventory.warehouse_id chargenumber bestbefore) ], + ); + + if ($params{by}) { + for (listify($params{by})) { + my $selects = $allowed_by{$_} or Carp::croak("unknown option for by: $_"); + push @selects, @$selects; + push @groups, @$selects; + } + } + + my $select = join ',', @selects; + my $where = @where ? 'WHERE ' . join ' AND ', @where : ''; + my $group_by = @groups ? 'GROUP BY ' . join ', ', @groups : ''; + + my $query = <<""; + SELECT $select FROM inventory + LEFT JOIN bin ON bin_id = bin.id + LEFT JOIN warehouse ON bin.warehouse_id = warehouse.id + $where + $group_by + + if ($onhand_mode) { + $query .= ' HAVING SUM(qty) > 0'; + } + + my $results = selectall_hashref_query($::form, SL::DB->client->dbh, $query, @values); + + my %with_objects = ( + part => 'SL::DB::Manager::Part', + bin => 'SL::DB::Manager::Bin', + warehouse => 'SL::DB::Manager::Warehouse', + ); + + my %slots = ( + part => 'parts_id', + bin => 'bin_id', + warehouse => 'warehouse_id', + ); + + if ($params{by} && $params{with_objects}) { + for my $with_object (listify($params{with_objects})) { + Carp::croak("unknown with_object $with_object") if !exists $with_objects{$with_object}; + + my $manager = $with_objects{$with_object}; + my $slot = $slots{$with_object}; + next if !(my @ids = map { $_->{$slot} } @$results); + my $objects = $manager->get_all(query => [ id => \@ids ]); + my %objects_by_id = map { $_->id => $_ } @$objects; + + $_->{$with_object} = $objects_by_id{$_->{$slot}} for @$results; + } + } + + if ($params{by}) { + return $results; + } else { + return $results->[0]{qty}; + } +} + +sub get_stock { + _get_stock_onhand(@_, onhand => 0); +} + +sub get_onhand { + _get_stock_onhand(@_, onhand => 1); +} + +sub allocate { + my (%params) = @_; + + croak('allocate needs a part') unless $params{part}; + croak('allocate needs a qty') unless $params{qty}; + + my $part = $params{part}; + my $qty = $params{qty}; + + return () if $qty <= 0; + + my $results = get_stock(part => $part, by => 'for_allocate'); + my %bin_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{bin}); + my %wh_whitelist = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{warehouse}); + my %chargenumbers = map { (ref $_ ? $_->id : $_) => 1 } grep defined, listify($params{chargenumber}); + + # sort results so that chargenumbers are matched first, then wanted bins, then wanted warehouses + my @sorted_results = sort { + exists $chargenumbers{$b->{chargenumber}} <=> exists $chargenumbers{$a->{chargenumber}} # then prefer wanted chargenumbers + || exists $bin_whitelist{$b->{bin_id}} <=> exists $bin_whitelist{$a->{bin_id}} # then prefer wanted bins + || exists $wh_whitelist{$b->{warehouse_id}} <=> exists $wh_whitelist{$a->{warehouse_id}} # then prefer wanted bins + || $a->{itime} <=> $b->{itime} # and finally prefer earlier charges + } @$results; + my @allocations; + my $rest_qty = $qty; + + for my $chunk (@sorted_results) { + my $qty = min($chunk->{qty}, $rest_qty); + + # since allocate operates on stock, this also ensures that no negative stock results are used + if ($qty > 0) { + push @allocations, SL::Helper::Inventory::Allocation->new( + parts_id => $chunk->{parts_id}, + qty => $qty, + comment => $params{comment}, + bin_id => $chunk->{bin_id}, + warehouse_id => $chunk->{warehouse_id}, + chargenumber => $chunk->{chargenumber}, + bestbefore => $chunk->{bestbefore}, + for_object_id => undef, + ); + $rest_qty -= _round_number($qty, 5); + } + $rest_qty = _round_number($rest_qty, 5); + last if $rest_qty == 0; + } + if ($rest_qty > 0) { + die SL::X::Inventory::Allocation->new( + error => 'not enough to allocate', + msg => t8("can not allocate #1 units of #2, missing #3 units", _format_number($qty), $part->displayable_name, _format_number($rest_qty)), + ); + } else { + if ($params{constraints}) { + check_constraints($params{constraints},\@allocations); + } + return @allocations; + } +} + +sub allocate_for_assembly { + my (%params) = @_; + + my $part = $params{part} or Carp::croak('allocate needs a part'); + my $qty = $params{qty} or Carp::croak('allocate needs a qty'); + + Carp::croak('not an assembly') unless $part->is_assembly; + + my %parts_to_allocate; + + for my $assembly ($part->assemblies) { + $parts_to_allocate{ $assembly->part->id } //= 0; + $parts_to_allocate{ $assembly->part->id } += $assembly->qty * $qty; + } + + my @allocations; + + for my $part_id (keys %parts_to_allocate) { + my $part = SL::DB::Part->load_cached($part_id); + push @allocations, allocate(%params, part => $part, qty => $parts_to_allocate{$part_id}); + } + + @allocations; +} + +sub check_constraints { + my ($constraints, $allocations) = @_; + if ('CODE' eq ref $constraints) { + if (!$constraints->(@$allocations)) { + die SL::X::Inventory::Allocation->new( + error => 'allocation constraints failure', + msg => t8("Allocations didn't pass constraints"), + ); + } + } else { + croak 'constraints needs to be a hashref' unless 'HASH' eq ref $constraints; + + my %supported_constraints = ( + bin_id => 'bin_id', + warehouse_id => 'warehouse_id', + chargenumber => 'chargenumber', + ); + + for (keys %$constraints ) { + croak "unsupported constraint '$_'" unless $supported_constraints{$_}; + next unless defined $constraints->{$_}; + + my %whitelist = map { (ref $_ ? $_->id : $_) => 1 } listify($constraints->{$_}); + my $accessor = $supported_constraints{$_}; + + if (any { !$whitelist{$_->$accessor} } @$allocations) { + my %error_constraints = ( + bin_id => t8('Bins'), + warehouse_id => t8('Warehouses'), + chargenumber => t8('Chargenumbers'), + ); + my @allocs = grep { $whitelist{$_->$accessor} } @$allocations; + my $needed = sum map { $_->qty } grep { !$whitelist{$_->$accessor} } @$allocations; + my $err = t8("Cannot allocate parts."); + $err .= ' '.t8('part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.', + SL::DB::Part->load_cached($_->parts_id)->description, + SL::DB::Bin->load_cached($_->bin_id)->full_description, + _format_number($_->qty), _format_number($needed), $_->chargenumber ? $_->chargenumber : '--') for @allocs; + die SL::X::Inventory::Allocation->new( + error => 'allocation constraints failure', + msg => $err, + ); + } + } + } +} + +sub produce_assembly { + my (%params) = @_; + + my $part = $params{part} or Carp::croak('produce_assembly needs a part'); + my $qty = $params{qty} or Carp::croak('produce_assembly needs a qty'); + + my $allocations = $params{allocations}; + if ($params{auto_allocate}) { + Carp::croak("produce_assembly: can't have both allocations and auto_allocate") if $params{allocations}; + $allocations = [ allocate_for_assembly(part => $part, qty => $qty) ]; + } else { + Carp::croak("produce_assembly: need allocations or auto_allocate to produce something") if !$params{allocations}; + $allocations = $params{allocations}; + } + + my $bin = $params{bin} or Carp::croak("need target bin"); + my $chargenumber = $params{chargenumber}; + my $bestbefore = $params{bestbefore}; + my $for_object_id = $params{for_object_id}; + my $comment = $params{comment} // ''; + + my $invoice = $params{invoice}; + my $project = $params{project}; + + my $shippingdate = $params{shippingsdate} // DateTime->now_local; + + my $trans_id = $params{trans_id}; + ($trans_id) = selectrow_query($::form, SL::DB->client->dbh, qq|SELECT nextval('id')| ) unless $trans_id; + + my $trans_type_out = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'used'); + my $trans_type_in = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'assembled'); + + # check whether allocations are sane + if (!$params{no_check_allocations} && !$params{auto_allocate}) { + my %allocations_by_part = map { $_->parts_id => $_->qty } @$allocations; + for my $assembly ($part->assemblies) { + $allocations_by_part{ $assembly->parts_id } -= $assembly->qty * $qty; + } + + die "allocations are insufficient for production" if any { $_ < 0 } values %allocations_by_part; + } + + my @transfers; + for my $allocation (@$allocations) { + my $oe_id = delete $allocation->{for_object_id}; + push @transfers, $allocation->transfer_object( + trans_id => $trans_id, + qty => -$allocation->qty, + trans_type => $trans_type_out, + shippingdate => $shippingdate, + employee => SL::DB::Manager::Employee->current, + ); + } + + push @transfers, SL::DB::Inventory->new( + trans_id => $trans_id, + trans_type => $trans_type_in, + part => $part, + qty => $qty, + bin => $bin, + warehouse => $bin->warehouse_id, + chargenumber => $chargenumber, + bestbefore => $bestbefore, + shippingdate => $shippingdate, + project => $project, + invoice => $invoice, + comment => $comment, + employee => SL::DB::Manager::Employee->current, + oe_id => $for_object_id, + ); + + SL::DB->client->with_transaction(sub { + $_->save for @transfers; + 1; + }) or do { + die SL::DB->client->error; + }; + + @transfers; +} + +sub default_show_bestbefore { + $::instance_conf->get_show_bestbefore +} + +1; + +=encoding utf-8 + +=head1 NAME + +SL::WH - Warehouse and Inventory API + +=head1 SYNOPSIS + + # See description for an intro to the concepts used here. + + use SL::Helper::Inventory qw(:ALL); + + # stock, get "what's there" for a part with various conditions: + my $qty = get_stock(part => $part); # how much is on stock? + my $qty = get_stock(part => $part, date => $date); # how much was on stock at a specific time? + my $qty = get_stock(part => $part, bin => $bin); # how much is on stock in a specific bin? + my $qty = get_stock(part => $part, warehouse => $warehouse); # how much is on stock in a specific warehouse? + my $qty = get_stock(part => $part, chargenumber => $chargenumber); # how much is on stock of a specific chargenumber? + + # onhand, get "what's available" for a part with various conditions: + my $qty = get_onhand(part => $part); # how much is available? + my $qty = get_onhand(part => $part, date => $date); # how much was available at a specific time? + my $qty = get_onhand(part => $part, bin => $bin); # how much is available in a specific bin? + my $qty = get_onhand(part => $part, warehouse => $warehouse); # how much is available in a specific warehouse? + my $qty = get_onhand(part => $part, chargenumber => $chargenumber); # how much is availbale of a specific chargenumber? + + # onhand batch mode: + my $data = get_onhand( + warehouse => $warehouse, + by => [ qw(bin part chargenumber) ], + with_objects => [ qw(bin part) ], + ); + + # allocate: + my @allocations = allocate( + part => $part, # part_id works too + qty => $qty, # must be positive + chargenumber => $chargenumber, # optional, may be arrayref. if provided these charges will be used first + bestbefore => $datetime, # optional, defaults to today. items with bestbefore prior to that date wont be used + bin => $bin, # optional, may be arrayref. if provided + ); + + # shortcut to allocate all that is needed for producing an assembly, will use chargenumbers as appropriate + my @allocations = allocate_for_assembly( + part => $assembly, # part_id works too + qty => $qty, # must be positive + ); + + # create allocation manually, bypassing checks. all of these need to be passed, even undefs + my $allocation = SL::Helper::Inventory::Allocation->new( + part_id => $part->id, + qty => 15, + bin_id => $bin_obj->id, + warehouse_id => $bin_obj->warehouse_id, + chargenumber => '1823772365', + bestbefore => undef, + for_object_id => $order->id, + ); + + # produce_assembly: + produce_assembly( + part => $part, # target assembly + qty => $qty, # qty + allocations => \@allocations, # allocations to use. alternatively use "auto_allocate => 1," + + # where to put it + bin => $bin, # needed unless a global standard target is configured + chargenumber => $chargenumber, # optional + bestbefore => $datetime, # optional + comment => $comment, # optional + ); + +=head1 DESCRIPTION + +New functions for the warehouse and inventory api. + +The WH api currently has three large shortcomings: It is very hard to just get +the current stock for an item, it's extremely complicated to use it to produce +assemblies while ensuring that no stock ends up negative, and it's very hard to +use it to get an overview over the actual contents of the inventory. + +The first problem has spawned several dozen small functions in the program that +try to implement that, and those usually miss some details. They may ignore +bestbefore times, comments, ignore negative quantities etc. + +To get this cleaned up a bit this code introduces two concepts: stock and onhand. + +=over 4 + +=item * Stock is defined as the actual contents of the inventory, everything that is +there. + +=item * Onhand is what is available, which means things that are stocked, +not expired and not in any other way reserved for other uses. + +=back + +The two new functions C and C encapsulate these principles and +allow simple access with some optional filters for chargenumbers or warehouses. +Both of them have a batch mode that can be used to get these information to +supplement simple reports. + +To address the safe assembly creation a new function has been added. +C will try to find the requested quantity of a part in the inventory +and will return allocations of it which can then be used to create the +assembly. Allocation will happen with the C semantics defined above, +meaning that by default no expired goods will be used. The caller can supply +hints of what shold be used and in those cases chargenumbers will be used up as +much as possible first. C will always try to fulfil the request even +beyond those. Should the required amount not be stocked, allocate will throw an +exception. + +C has been rewritten to only accept parameters about the +target of the production, and requires allocations to complete the request. The +allocations can be supplied manually, or can be generated automatically. +C will check whether enough allocations are given to create +the assembly, but will not check whether the allocations are backed. If the +allocations are not sufficient or if the auto-allocation fails an exception +is returned. If you need to produce something that is not in the inventory, you +can bypass those checks by creating the allocations yourself (see +L). + +Note: this is only intended to cover the scenarios described above. For other cases: + +=over 4 + +=item * + +If you need actual inventory objects because of record links or something like +that load them directly. And strongly consider redesigning that, because it's +really fragile. + +=item * + +You need weight or accounting information you're on your own. The inventory api +only concerns itself with the raw quantities. + +=item * + +If you need the first stock date of parts, or anything related to a specific +transfer type or direction, this is not covered yet. + +=back + +=head1 FUNCTIONS + +=over 4 + +=item * get_stock PARAMS + +Returns for single parts how much actually exists in the inventory. + +Options: + +=over 4 + +=item * part + +The part. Must be present without C. May be arrayref with C. Can be object or id. + +=item * bin + +If given, will only return stock on these bins. Optional. May be array, May be object or id. + +=item * warehouse + +If given, will only return stock on these warehouses. Optional. May be array, May be object or id. + +=item * date + +If given, will return stock as it were on this timestamp. Optional. Must be L object. + +=item * chargenumber + +If given, will only show stock with this chargenumber. Optional. May be array. + +=item * by + +See L + +=item * with_objects + +See L + +=back + +Will return a single qty normally, see L for batch +mode when C is given. + +=item * get_onhand PARAMS + +Returns for single parts how much is available in the inventory. That excludes +stock with expired bestbefore. + +It takes the same options as L. + +=over 4 + +=item * bestbefore + +If given, will only return stock with a bestbefore at or after the given date. +Optional. Must be L object. + +=back + +=item * allocate PARAMS + +Accepted parameters: + +=over 4 + +=item * part + +=item * qty + +=item * bin + +Bin object. Optional. + +=item * warehouse + +Warehouse object. Optional. + +=item * chargenumber + +Optional. + +=item * bestbefore + +Datetime. Optional. + +=back + +Tries to allocate the required quantity using what is currently onhand. If +given any of C, C, C + +=item * allocate_for_assembly PARAMS + +Shortcut to allocate everything for an assembly. Takes the same arguments. Will +compute the required amount for each assembly part and allocate all of them. + +=item * produce_assembly + + +=back + +=head1 STOCK/ONHAND REPORT MODE + +If the special option C is given with an arrayref, the result will instead +be an arrayref of partitioned stocks by those fields. Valid partitions are: + +=over 4 + +=item * part + +If this is given, part is optional in the parameters + +=item * bin + +=item * warehouse + +=item * chargenumber + +=item * bestbefore + +=back + +Note: If you want to use the returned data to create allocations you I to +enable all of these. To make this easier a special shortcut exists + +In this mode, C can be used to load C, C, +C objects in one go, just like with Rose. They +need to be present in C before that though. + +=head1 ALLOCATION ALGORITHM + +When calling allocate, the current onhand (== available stock) of the item will +be used to decide which bins/chargenumbers/bestbefore can be used. + +In general allocate will try to make the request happen, and will use the +provided charges up first, and then tap everything else. If you need to only +I use the provided charges, you'll need to craft the allocations +yourself. See L for that. + +If C is given, those will be used up next. + +After that normal quantities will be used. + +These are tiebreakers and expected to rarely matter in reality. If you need +finegrained control over which allocation is used, you may want to get the +onhands yourself and select the appropriate ones. + +Only quantities with C unset or after the given date will be +considered. If more than one charge is eligible, the earlier C +will be used. + +Allocations do NOT have an internal memory and can't react to other allocations +of the same part earlier. Never double allocate the same part within a +transaction. + +=head1 ALLOCATION DATA STRUCTURE + +Allocations are instances of the helper class C. They require +each of the following attributes to be set at creation time: + +=over 4 + +=item * parts_id + +=item * qty + +=item * bin_id + +=item * warehouse_id + +=item * chargenumber + +=item * bestbefore + +=item * for_object_id + +If set the allocations will be marked as allocated for the given object. +If these allocations are later used to produce an assembly, the resulting +consuming transactions will be marked as belonging to the given object. +The object may be an order, productionorder or other objects + +=back + +C, C and C and C may be +C (but must still be present at creation time). Instances are considered +immutable. + +Allocations also provide the method C which will create a new +C bject with all the playload. + +=head1 CONSTRAINTS + + # whitelist constraints + ->allocate( + ... + constraints => { + bin_id => \@allowed_bins, + chargenumber => \@allowed_chargenumbers, + } + ); + + # custom constraints + ->allocate( + constraints => sub { + # only allow chargenumbers with specific format + all { $_->chargenumber =~ /^ C \d{8} - \a{d2} $/x } @_ + + && + # and must all have a bestbefore date + all { $_->bestbefore } @_; + } + ) + +C is "best effort" in nature. It will take the C, +C etc hints from the parameters, but will try it's bvest to +fulfil the request anyway and only bail out if it is absolutely not possible. + +Sometimes you need to restrict allocations though. For this you can pass +additional constraints to C. A constraint serves as a whitelist. +Every allocation must fulfil every constraint by having that attribute be one +of the given values. + +In case even that is not enough, you may supply a custom check by passing a +function that will be given the allocation objects. + +Note that both whitelists and constraints do not influence the order of +allocations, which is done purely from the initial parameters. They only serve +to reject allocations made in good faith which do fulfil required assertions. + +=head1 ERROR HANDLING + +C and C will throw exceptions if the request can +not be completed. The usual reason will be insufficient onhand to allocate, or +insufficient allocations to process the request. + +=head1 KNOWN PROBLEMS + + * It's not currently possible to identify allocations between requests, for + example for presenting the user possible allocations and then actually using + them on the next request. + * It's not currently possible to give C prior constraints. + Currently all constraints are treated as hints (and will be preferred) but + the internal ordering of the hints is fixed and more complex preferentials + are not supported. + * bestbefore handling is untested + * interaction with config option "transfer_default_ignore_onhand" is + currently undefined (and implicitly ignores it) + +=head1 TODO + + * define and describe error classes + * define wrapper classes for stock/onhand batch mode return values + * handle extra arguments in produce: shippingdate, project + * document no_ check + * tests + +=head1 BUGS + +None yet :) + +=head1 AUTHOR + +Sven Schöling Esven.schoeling@googlemail.comE + +=cut diff --git a/SL/Helper/Inventory/Allocation.pm b/SL/Helper/Inventory/Allocation.pm new file mode 100644 index 000000000..e406bf40c --- /dev/null +++ b/SL/Helper/Inventory/Allocation.pm @@ -0,0 +1,65 @@ +package SL::Helper::Inventory::Allocation; + +use strict; + +my @attributes = qw(parts_id qty bin_id warehouse_id chargenumber bestbefore comment for_object_id); +my %attributes = map { $_ => 1 } @attributes; +my %mapped_attributes = ( + for_object_id => 'oe_id', +); + +for my $name (@attributes) { + no strict 'refs'; + *{"SL::Helper::Inventory::Allocation::$name"} = sub { $_[0]{$name} }; +} + +sub new { + my ($class, %params) = @_; + + Carp::croak("missing attribute $_") for grep { !exists $params{$_} } @attributes; + Carp::croak("unknown attribute $_") for grep { !exists $attributes{$_} } keys %params; + Carp::croak("$_ must be set") for grep { !$params{$_} } qw(parts_id qty bin_id); + Carp::croak("$_ must be positive") for grep { !($params{$_} > 0) } qw(parts_id qty bin_id); + + bless { %params }, $class; +} + +sub transfer_object { + my ($self, %params) = @_; + + SL::DB::Inventory->new( + (map { + my $attr = $mapped_attributes{$_} // $_; + $attr => $self->{$attr} + } @attributes), + %params, + ); +} + +1; + +=encoding utf-8 + +=head1 NAME + +SL::Helper::Inventory::Allocation - Inventory API allocation data structure + +=head1 SYNOPSIS + + # all of these need to be present + my $allocation = SL::Helper::Inventory::Allocation->new( + part_id => $part->id, + qty => 15, + bin_id => $bin_obj->id, + warehouse_id => $bin_obj->warehouse_id, + chargenumber => '1823772365', # can be undef + bestbefore => undef, # can be undef + for_object_id => $order->id, # can be undef + ); + + +=head1 SEE ALSO + +The full documentation can be found in L + +=cut diff --git a/SL/IC.pm b/SL/IC.pm index 868fb7417..ca03e5144 100644 --- a/SL/IC.pm +++ b/SL/IC.pm @@ -739,37 +739,14 @@ sub retrieve_accounts { # transdate madness. my $transdate = ""; - if ($form->{type} eq "invoice" or $form->{type} eq "credit_note") { + if (($form->{type} eq "invoice") or ($form->{type} eq "credit_note") or ($form->{script} eq 'ir.pl')) { # use deliverydate for sales and purchase invoice, if it exists # also use deliverydate for credit notes - if (!$form->{deliverydate}) { - $transdate = $form->{invdate}; - } else { - $transdate = $form->{deliverydate}; - } - } elsif ($form->{script} eq 'ir.pl') { - # when a purchase invoice is opened from the report of purchase invoices - # $form->{type} isn't set, but $form->{script} is, not sure why this is or - # whether this distinction matters in some other scenario. Otherwise one - # could probably take out this elsif and add a - # " or $form->{script} eq 'ir.pl' " - # to the above if-statement - if (!$form->{deliverydate}) { - $transdate = $form->{invdate}; - } else { - $transdate = $form->{deliverydate}; - } - } elsif (($form->{type} eq "credit_note") and $form->{deliverydate}) { - # if credit_note has a deliverydate, use this instead of invdate - # useful for credit_notes of invoices from an old period with different tax - # if there is no deliverydate then invdate is used, old default (see next elsif) - # Falls hier der Stichtag für Steuern anders bestimmt wird, - # entsprechend auch bei Taxkeys.pm anpassen - $transdate = $form->{deliverydate}; - } elsif (($form->{type} eq "credit_note") || ($form->{script} eq 'ir.pl')) { - $transdate = $form->{invdate}; + $transdate = $form->{tax_point} || $form->{deliverydate} || $form->{invdate}; } else { - $transdate = $form->{transdate}; + my $deliverydate; + $deliverydate = $form->{reqdate} if any { $_ eq $form->{type} } qw(sales_order request_quotation purchase_order); + $transdate = $form->{tax_point} || $deliverydate || $form->{transdate}; } if ($transdate eq "") { diff --git a/SL/IR.pm b/SL/IR.pm index 5c768cf8a..2663ad676 100644 --- a/SL/IR.pm +++ b/SL/IR.pm @@ -549,7 +549,7 @@ SQL if ($form->{currency} ne $defaultcurrency) && !$exchangerate; # record acc_trans transactions - my $taxdate = $form->{deliverydate} ? $form->{deliverydate} : $form->{invdate}; + my $taxdate = $form->{tax_point} || $form->{deliverydate} || $form->{invdate}; foreach my $trans_id (keys %{ $form->{amount} }) { foreach my $accno (keys %{ $form->{amount}{$trans_id} }) { $form->{amount}{$trans_id}{$accno} = $form->round_amount($form->{amount}{$trans_id}{$accno}, 2); @@ -735,7 +735,7 @@ SQL orddate = ?, quodate = ?, vendor_id = ?, amount = ?, netamount = ?, paid = ?, duedate = ?, deliverydate = ?, invoice = ?, taxzone_id = ?, notes = ?, taxincluded = ?, - intnotes = ?, storno_id = ?, storno = ?, + intnotes = ?, storno_id = ?, storno = ?, tax_point = ?, cp_id = ?, employee_id = ?, department_id = ?, delivery_term_id = ?, payment_id = ?, currency_id = (SELECT id FROM currencies WHERE name = ?), @@ -746,7 +746,7 @@ SQL conv_date($form->{orddate}), conv_date($form->{quodate}), conv_i($form->{vendor_id}), $amount, $netamount, $form->{paid}, conv_date($form->{duedate}), conv_date($form->{deliverydate}), '1', $taxzone_id, $restricter->process($form->{notes}), $form->{taxincluded} ? 't' : 'f', - $form->{intnotes}, conv_i($form->{storno_id}), $form->{storno} ? 't' : 'f', + $form->{intnotes}, conv_i($form->{storno_id}), $form->{storno} ? 't' : 'f', conv_date($form->{tax_point}), conv_i($form->{cp_id}), conv_i($form->{employee_id}), conv_i($form->{department_id}), conv_i($form->{delivery_term_id}), conv_i($form->{payment_id}), $form->{"currency"}, @@ -1002,7 +1002,7 @@ sub retrieve_invoice { # retrieve invoice $query = qq|SELECT cp_id, invnumber, transdate AS invdate, duedate, - orddate, quodate, deliverydate, globalproject_id, + orddate, quodate, deliverydate, tax_point, globalproject_id, ordnumber, quonumber, paid, taxincluded, notes, taxzone_id, storno, gldate, mtime, itime, intnotes, (SELECT cu.name FROM currencies cu WHERE cu.id=ap.currency_id) AS currency, direct_debit, @@ -1022,7 +1022,7 @@ sub retrieve_invoice { delete $ref->{id}; map { $form->{$_} = $ref->{$_} } keys %$ref; - my $transdate = $form->{invdate} ? $dbh->quote($form->{invdate}) : "current_date"; + my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) :$form->{invdate} ? $dbh->quote($form->{invdate}) : "current_date"; my $taxzone_id = $form->{taxzone_id} * 1; $taxzone_id = SL::DB::Manager::TaxZone->get_default->id unless SL::DB::Manager::TaxZone->find_by(id => $taxzone_id); diff --git a/SL/IS.pm b/SL/IS.pm index e9e9144d4..5fe66cf47 100644 --- a/SL/IS.pm +++ b/SL/IS.pm @@ -1037,7 +1037,7 @@ SQL $project_id = conv_i($form->{"globalproject_id"}); # entsprechend auch beim Bestimmen des Steuerschlüssels in Taxkey.pm berücksichtigen - my $taxdate = $form->{deliverydate} ? $form->{deliverydate} : $form->{invdate}; + my $taxdate = $form->{tax_point} ||$form->{deliverydate} || $form->{invdate}; foreach my $trans_id (keys %{ $form->{amount_cogs} }) { foreach my $accno (keys %{ $form->{amount_cogs}{$trans_id} }) { @@ -1312,7 +1312,7 @@ SQL $query = qq|UPDATE ar set invnumber = ?, ordnumber = ?, quonumber = ?, cusordnumber = ?, - transdate = ?, orddate = ?, quodate = ?, customer_id = ?, + transdate = ?, orddate = ?, quodate = ?, tax_point = ?, customer_id = ?, amount = ?, netamount = ?, paid = ?, duedate = ?, deliverydate = ?, invoice = ?, shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, @@ -1327,7 +1327,7 @@ SQL delivery_term_id = ? WHERE id = ?|; @values = ( $form->{"invnumber"}, $form->{"ordnumber"}, $form->{"quonumber"}, $form->{"cusordnumber"}, - conv_date($form->{"invdate"}), conv_date($form->{"orddate"}), conv_date($form->{"quodate"}), conv_i($form->{"customer_id"}), + conv_date($form->{"invdate"}), conv_date($form->{"orddate"}), conv_date($form->{"quodate"}), conv_date($form->{tax_point}), conv_i($form->{"customer_id"}), $amount, $netamount, $form->{"paid"}, conv_date($form->{"duedate"}), conv_date($form->{"deliverydate"}), '1', $form->{"shippingpoint"}, $form->{"shipvia"}, $restricter->process($form->{"notes"}), $form->{"intnotes"}, @@ -2012,7 +2012,7 @@ sub _retrieve_invoice { qq|SELECT a.invnumber, a.ordnumber, a.quonumber, a.cusordnumber, a.orddate, a.quodate, a.globalproject_id, - a.transdate AS invdate, a.deliverydate, a.paid, a.storno, a.storno_id, a.gldate, + a.transdate AS invdate, a.deliverydate, a.tax_point, a.paid, a.storno, a.storno_id, a.gldate, a.shippingpoint, a.shipvia, a.notes, a.intnotes, a.taxzone_id, a.duedate, a.taxincluded, (SELECT cu.name FROM currencies cu WHERE cu.id=a.currency_id) AS currency, a.shipto_id, a.cp_id, a.employee_id, a.salesman_id, a.payment_id, @@ -2055,7 +2055,8 @@ sub _retrieve_invoice { $sth->finish; map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued); - my $transdate = $form->{deliverydate} ? $dbh->quote($form->{deliverydate}) + my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) + : $form->{deliverydate} ? $dbh->quote($form->{deliverydate}) : $form->{invdate} ? $dbh->quote($form->{invdate}) : "current_date"; diff --git a/SL/LXDebug.pm b/SL/LXDebug.pm index 0503dcb6c..a1e19767c 100644 --- a/SL/LXDebug.pm +++ b/SL/LXDebug.pm @@ -19,8 +19,9 @@ use constant FILE_TARGET => 0; use constant STDERR_TARGET => 1; use Data::Dumper; +use List::MoreUtils qw(all); use POSIX qw(strftime getpid); -use Scalar::Util qw(blessed refaddr weaken); +use Scalar::Util qw(blessed refaddr weaken looks_like_number); use Time::HiRes qw(gettimeofday tv_interval); use SL::Request (); use SL::YAML; @@ -231,8 +232,14 @@ sub dump_sql_result { map { $column_lengths{$_} = length $row->{$_} if (length $row->{$_} > $column_lengths{$_}) } keys %{ $row }; } + my %alignment; + foreach my $column (keys %column_lengths) { + my $all_look_like_number = all { (($_->{$column} // '') eq '') || looks_like_number($_->{$column}) } @{ $results }; + $alignment{$column} = $all_look_like_number ? '' : '-'; + } + my @sorted_names = sort keys %column_lengths; - my $format = join '|', map { '%' . $column_lengths{$_} . 's' } @sorted_names; + my $format = join '|', map { '%' . $alignment{$_} . $column_lengths{$_} . 's' } @sorted_names; $prefix .= ' ' if $prefix; diff --git a/SL/OE.pm b/SL/OE.pm index 974a1a1bd..759f96265 100644 --- a/SL/OE.pm +++ b/SL/OE.pm @@ -743,7 +743,7 @@ SQL $query = qq|UPDATE oe SET ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?, - customer_id = ?, amount = ?, netamount = ?, reqdate = ?, taxincluded = ?, + customer_id = ?, amount = ?, netamount = ?, reqdate = ?, tax_point = ?, taxincluded = ?, shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?, delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?, taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?, @@ -754,7 +754,7 @@ SQL @values = ($form->{ordnumber} || '', $form->{quonumber}, $form->{cusordnumber}, conv_date($form->{transdate}), conv_i($form->{vendor_id}), conv_i($form->{customer_id}), - $amount, $netamount, conv_date($reqdate), + $amount, $netamount, conv_date($reqdate), conv_date($form->{tax_point}), $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint}, $form->{shipvia}, $restricter->process($form->{notes}), $form->{intnotes}, $form->{currency}, $form->{closed} ? 't' : 'f', @@ -1017,7 +1017,7 @@ sub _retrieve { o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes, (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id, o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal, - o.closed, o.reqdate, o.quonumber, o.department_id, o.cusordnumber, + o.closed, o.reqdate, o.tax_point, o.quonumber, o.department_id, o.cusordnumber, o.mtime, o.itime, d.description AS department, o.payment_id, o.language_id, o.taxzone_id, o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id, @@ -1096,7 +1096,7 @@ sub _retrieve { map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued); } # if !@ids - my $transdate = $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date"; + my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) : $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date"; $form->{taxzone_id} = 0 unless ($form->{taxzone_id}); unshift @values, ($form->{taxzone_id}) x 2; diff --git a/SL/Presenter/Tag.pm b/SL/Presenter/Tag.pm index 2b45d97d1..8929dc5e2 100644 --- a/SL/Presenter/Tag.pm +++ b/SL/Presenter/Tag.pm @@ -370,10 +370,11 @@ sub date_tag { $::request->layout->add_javascripts('kivi.Validator.js'); $::request->presenter->need_reinit_widgets($params{id}); + $params{'data-validate'} = join(' ', "date", grep { $_ } (delete $params{'data-validate'})); + input_tag( $name, blessed($value) ? $value->to_lxoffice : $value, size => 11, - "data-validate" => "date", %params, %class, @onchange, ); diff --git a/SL/X.pm b/SL/X.pm index 9343deab0..c3533e2ef 100644 --- a/SL/X.pm +++ b/SL/X.pm @@ -30,6 +30,16 @@ use Exception::Class ( 'SL::X::ZUGFeRDValidation' => { isa => 'SL::X::Base', }, + 'SL::X::Inventory' => { + isa => 'SL::X::Base', + fields => [ qw(msg error) ], + defaults => { error_template => [ '%s: %s', qw(msg) ] }, + }, + 'SL::X::Inventory::Allocation' => { + isa => 'SL::X::Base', + fields => [ qw(msg error) ], + defaults => { error_template => [ '%s: %s', qw(msg) ] }, + }, ); 1; diff --git a/bin/mozilla/ap.pl b/bin/mozilla/ap.pl index 2fe8041c9..afc2ab558 100644 --- a/bin/mozilla/ap.pl +++ b/bin/mozilla/ap.pl @@ -48,6 +48,7 @@ use SL::DB::Chart; use SL::DB::Currency; use SL::DB::Default; use SL::DB::Order; +use SL::DB::PaymentTerm; use SL::DB::PurchaseInvoice; use SL::DB::RecordTemplate; use SL::DB::Tax; @@ -129,6 +130,7 @@ sub load_record_template { $::form->{currency} = $template->currency->name; $::form->{direct_debit} = $template->direct_debit; $::form->{globalproject_id} = $template->project_id; + $::form->{payment_id} = $template->payment_id; $::form->{AP_chart_id} = $template->ar_ap_chart_id; $::form->{transdate} = $today->to_kivitendo; $::form->{duedate} = $today->to_kivitendo; @@ -212,6 +214,7 @@ sub save_record_template { vendor_id => $::form->{vendor_id} || undef, department_id => $::form->{department_id} || undef, project_id => $::form->{globalproject_id} || undef, + payment_id => $::form->{payment_id} || undef, taxincluded => $::form->{taxincluded} ? 1 : 0, direct_debit => $::form->{direct_debit} ? 1 : 0, ordnumber => $::form->{ordnumber}, @@ -252,7 +255,12 @@ sub add { $form->{transdate} = $form->{initial_transdate}; if ($form->{vendor_id}) { - my $last_used_ap_chart = SL::DB::Vendor->load_cached($form->{vendor_id})->last_used_ap_chart; + my $vendor = SL::DB::Vendor->load_cached($form->{vendor_id}); + + # set initial payment terms + $form->{payment_id} = $vendor->payment_id; + + my $last_used_ap_chart = $vendor->last_used_ap_chart; $form->{"AP_amount_chart_id_1"} = $last_used_ap_chart->id if $last_used_ap_chart; } @@ -443,7 +451,7 @@ sub form_header { my $follow_up_vc = $form->{vendor_id} ? SL::DB::Vendor->load_cached($form->{vendor_id})->name : ''; my $follow_up_trans_info = "$form->{invnumber} ($follow_up_vc)"; - $::request->layout->add_javascripts("autocomplete_chart.js", "show_vc_details.js", "show_history.js", "follow_up.js", "kivi.Draft.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.File.js", "kivi.AP.js", "kivi.CustomerVendor.js", "kivi.Validator.js", "autocomplete_project.js"); + $::request->layout->add_javascripts("autocomplete_chart.js", "show_vc_details.js", "show_history.js", "follow_up.js", "kivi.Draft.js", "kivi.SalesPurchase.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.File.js", "kivi.AP.js", "kivi.CustomerVendor.js", "kivi.Validator.js", "autocomplete_project.js"); # $form->{totalpaid} is used by the action bar setup to determine # whether or not canceling is allowed. Therefore it must be # calculated prior to the action bar setup. @@ -556,6 +564,7 @@ sub form_header { print $form->parse_html_template('ap/form_header', { today => DateTime->today, currencies => SL::DB::Manager::Currency->get_all_sorted, + payment_terms => SL::DB::Manager::PaymentTerm->get_all_sorted(query => [ or => [ obsolete => 0, id => $::form->{payment_id}*1 ]]), }); $main::lxdebug->leave_sub(); @@ -656,8 +665,14 @@ sub update { if (($form->{previous_vendor_id} || $form->{vendor_id}) != $form->{vendor_id}) { IR->get_vendor(\%::myconfig, $form); + + my $vendor = SL::DB::Vendor->load_cached($form->{vendor_id}); + + # reset payment to new vendor + $form->{payment_id} = $vendor->payment_id; + if (($form->{rowcount} == 1) && ($form->{amount_1} == 0)) { - my $last_used_ap_chart = SL::DB::Vendor->load_cached($form->{vendor_id})->last_used_ap_chart; + my $last_used_ap_chart = $vendor->last_used_ap_chart; $form->{"AP_amount_chart_id_1"} = $last_used_ap_chart->id if $last_used_ap_chart; } } diff --git a/bin/mozilla/gl.pl b/bin/mozilla/gl.pl index 256fe1162..6f87019c9 100644 --- a/bin/mozilla/gl.pl +++ b/bin/mozilla/gl.pl @@ -792,13 +792,6 @@ sub display_rows { $form->{totaldebit} = 0; $form->{totalcredit} = 0; - my %project_labels = (); - my @project_values = (""); - foreach my $item (@{ $form->{"ALL_PROJECTS"} }) { - push(@project_values, $item->{"id"}); - $project_labels{$item->{"id"}} = $item->{"projectnumber"}; - } - my %charts_by_id = map { ($_->{id} => $_) } @{ $::form->{ALL_CHARTS} }; my $default_chart = $::form->{ALL_CHARTS}[0]; my $transdate = $::form->{transdate} ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local; @@ -914,13 +907,8 @@ sub display_rows { } } - my $projectnumber = - NTI($cgi->popup_menu('-name' => "project_id_$i", - '-values' => \@project_values, - '-labels' => \%project_labels, - '-default' => $form->{"project_id_$i"} )); - my $projectnumber_hidden = qq| - |; + my $projectnumber = SL::Presenter::Project::picker("project_id_$i", $form->{"project_id_$i"}); + my $projectnumber_hidden = SL::Presenter::Tag::hidden_tag("project_id_$i", $form->{"project_id_$i"}); my $copy2credit = $i == 1 ? 'onkeyup="copy_debit_to_credit()"' : ''; my $balance = $form->format_amount(\%::myconfig, $balances{$accno_id} // 0, 2, 'DRCR'); @@ -1099,16 +1087,15 @@ sub form_header { my ($init) = @_; - $::request->layout->add_javascripts("autocomplete_chart.js", "kivi.File.js", "kivi.GL.js", "kivi.RecordTemplate.js"); - - my @old_project_ids = grep { $_ } map{ $::form->{"project_id_$_"} } 1..$::form->{rowcount}; + $::request->layout->add_javascripts("autocomplete_chart.js", "autocomplete_project.js", "kivi.File.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.Validator.js"); - $::form->get_lists("projects" => { "key" => "ALL_PROJECTS", - "all" => 0, - "old_id" => \@old_project_ids }, + my @old_project_ids = grep { $_ } map{ $::form->{"project_id_$_"} } 1..$::form->{rowcount}; + my @conditions = @old_project_ids ? (id => \@old_project_ids) : (); + $::form->{ALL_PROJECTS} = SL::DB::Manager::Project->get_all_sorted(query => [ or => [ active => 1, @conditions ]]); - "charts" => { "key" => "ALL_CHARTS", - "transdate" => $::form->{transdate} }); + $::form->get_lists( + "charts" => { "key" => "ALL_CHARTS", "transdate" => $::form->{transdate} }, + ); # we cannot book on charttype header @{ $::form->{ALL_CHARTS} } = grep { $_->{charttype} ne 'H' } @{ $::form->{ALL_CHARTS} }; diff --git a/bin/mozilla/io.pl b/bin/mozilla/io.pl index 3f43c15c8..0a6b861d0 100644 --- a/bin/mozilla/io.pl +++ b/bin/mozilla/io.pl @@ -328,7 +328,7 @@ sub display_row { $ship_qty /= ( $all_units->{$form->{"unit_$i"}}->{factor} || 1 ); $column_data{ship} = $form->format_amount(\%myconfig, $form->round_amount($ship_qty, 2) * 1) . ' ' . $form->{"unit_$i"} - . $cgi->hidden(-name => "ship_$i", -value => $form->format_amount(\%myconfig, $form->{"ship_$i"}, $qty_dec)); + . $cgi->hidden(-name => "ship_$i", -value => $form->{"ship_$i"}, $qty_dec); my $ship_missing_qty = $form->{"qty_$i"} - $ship_qty; my $ship_missing_amount = $form->round_amount($ship_missing_qty * $form->{"sellprice_$i"} * (100 - $form->{"discount_$i"}) / 100 / $price_factor, 2); @@ -1409,7 +1409,7 @@ sub print_form { # Format dates. format_dates($output_dateformat, $output_longdates, - qw(invdate orddate quodate pldate duedate reqdate transdate + qw(invdate orddate quodate pldate duedate reqdate transdate tax_point shippingdate deliverydate validitydate paymentdate datepaid transdate_oe transdate_do transdate_quo deliverydate_oe dodate employee_startdate employee_enddate diff --git a/bin/mozilla/oe.pl b/bin/mozilla/oe.pl index 82f2906a6..d2cceeaf3 100644 --- a/bin/mozilla/oe.pl +++ b/bin/mozilla/oe.pl @@ -2050,8 +2050,7 @@ sub delivery_order { $main::lxdebug->leave_sub(); } -sub oe_delivery_order_from_order { - +sub oe_prepare_xyz_from_order { return if !$::form->{id}; my $order = SL::DB::Order->new(id => $::form->{id})->load; @@ -2068,27 +2067,15 @@ sub oe_delivery_order_from_order { $::form->{rowcount}++; _update_ship(); +} + +sub oe_delivery_order_from_order { + oe_prepare_xyz_from_order(); delivery_order(); } sub oe_invoice_from_order { - - return if !$::form->{id}; - - my $order = SL::DB::Order->new(id => $::form->{id})->load; - $order->flatten_to_form($::form, format_amounts => 1); - - # hack: add partsgroup for first row if it does not exists, - # because _remove_billed_or_delivered_rows and _remove_full_delivered_rows - # determine fields to handled by existing fields for the first row. If partsgroup - # is missing there, for deleted rows the partsgroup_field is not emptied and in - # update_delivery_order it will not considered an empty row ... - $::form->{partsgroup_1} = '' if !exists $::form->{partsgroup_1}; - - # fake last empty row - $::form->{rowcount}++; - - _update_ship(); + oe_prepare_xyz_from_order(); invoice(); } @@ -2259,7 +2246,7 @@ sub _remove_full_delivered_rows { next unless $::form->{"id_$row"}; my $base_factor = SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor; my $base_qty = $::form->parse_amount(\%::myconfig, $::form->{"qty_$row"}) * $base_factor; - my $ship_qty = $::form->parse_amount(\%::myconfig, $::form->{"ship_$row"}) * $base_factor; + my $ship_qty = $::form->{"ship_$row"} * $base_factor; #$main::lxdebug->message(LXDebug->DEBUG2(),"shipto=".$ship_qty." qty=".$base_qty); if (!$ship_qty || ($ship_qty < $base_qty)) { diff --git a/bin/mozilla/sepa.pl b/bin/mozilla/sepa.pl index b78083e99..ca4ad0aa1 100755 --- a/bin/mozilla/sepa.pl +++ b/bin/mozilla/sepa.pl @@ -749,7 +749,7 @@ sub setup_sepa_edit_transfer_action_bar { accesskey => 'enter', tooltip => t8('Post payments for selected invoices'), checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ], - only_if => $params{show_post_payments_button}, + disabled => $params{show_post_payments_button} ? undef : t8('All payments have already been posted.'), ], action => [ t8('Payment list'), @@ -757,7 +757,7 @@ sub setup_sepa_edit_transfer_action_bar { accesskey => 'enter', tooltip => t8('Download list of payments as PDF'), checks => [ [ 'kivi.check_if_entries_selected', '[name="ids[]"]' ] ], - not_if => $params{show_post_payments_button}, + disabled => $params{show_post_payments_button} ? t8('All payments must be posted before the payment list can be downloaded.') : undef, ], ); } diff --git a/doc/changelog b/doc/changelog index 42bbf5dd7..85127ab13 100644 --- a/doc/changelog +++ b/doc/changelog @@ -12,6 +12,10 @@ Mittelgroße neue Features: Kleinere neue Features und Detailverbesserungen: + - Inventur-Makse: Part-Picker sucht auch nach Lieferanten-Artikelnummer + - Einkaufs-/Verkaufsbelege und Buchungsmasken: Neues Feld Leistungsdatum, + welches die Steuerberechnung beeinflusst. I.d.R. gilt für die Steuer: + Leistungsdatum. Wenn leer, dann Lieferdatum; wenn leer, dann Belegdatum. - Neuer Order-Controller: Unterstützung für Übersetzungen von Artikeln wurde implementiert. - Einkaufs-/Verkaufsbelege: die Belegsprache ist nun als Auswahl diff --git a/locale/de/all b/locale/de/all index 19609099d..e2850f593 100755 --- a/locale/de/all +++ b/locale/de/all @@ -263,6 +263,8 @@ $self->{texts} = { 'All groups' => 'Alle Gruppen', 'All modules' => 'Alle Module', 'All partsgroups' => 'Alle Warengruppen', + 'All payments have already been posted.' => 'Es wurden bereits alle Zahlungen verbucht.', + 'All payments must be posted before the payment list can be downloaded.' => 'Alle Zahlungen müssen verbucht werden, bevor die Zahlungsliste heruntergeladen werden kann.', 'All price sources' => 'Alle Preisquellen', 'All reports' => 'Alle Berichte (Kontenübersicht, Summen- u. Saldenliste, Erfolgsrechnung, GuV, BWA, Bilanz, Projektbuchungen)', 'All the other clients will start with an empty set of WebDAV folders.' => 'Alle anderen Mandanten werden mit einem leeren Satz von Dokumenten-Ordnern ausgestattet.', @@ -270,6 +272,7 @@ $self->{texts} = { 'All transactions' => 'Alle Buchungen', 'All units have either no or exactly one base unit of which they are multiples.' => 'Einheiten haben entweder keine oder genau eine Basiseinheit, von der sie ein Vielfaches sind.', 'All users' => 'Alle BenutzerInnen', + 'Allocations didn\'t pass constraints' => 'Keine Verfügbarkeit wegen Lagereinschränkung', 'Allow access' => 'Zugriff erlauben', 'Allow conversion from sales orders to sales invoices' => 'Umwandlung von Verkaufsaufträgen in Verkaufsrechnungen zulassen', 'Allow conversion from sales quotations to sales invoices' => 'Umwandlung von Verkaufsangeboten in Verkaufsrechnungen zulassen', @@ -533,6 +536,7 @@ $self->{texts} = { 'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren', 'Cancel Accounts Receivables Transaction' => 'Debitorenbuchung stornieren', 'Cancelling is disallowed. Either undo or balance the current payments until the open amount matches the invoice amount' => 'Storno verboten, da Zahlungen zum Beleg vorhanden sind. Entweder die Zahlungen löschen oder mit umgekehrten Vorzeichen ausbuchen, sodass der offene Betrag dem Rechnungsbetrag entspricht.', + 'Cannot allocate parts.' => 'Es sind nicht genügend Artikel vorhanden', 'Cannot change transaction in a closed period!' => 'In einem bereits abgeschlossenen Zeitraum kann keine Buchung verändert werden!', 'Cannot check correct WebDAV folder' => 'Kann nicht den richtigen WebDAV Pfad überprüfen', 'Cannot delete account!' => 'Konto kann nicht gelöscht werden!', @@ -598,6 +602,7 @@ $self->{texts} = { 'Charge' => 'Berechnen', 'Charge Number' => 'Chargennummer', 'Charge number' => 'Chargennummer', + 'Chargenumbers' => 'Chargennummern', 'Charset' => 'Zeichensatz', 'Chart' => 'Buchungskonto', 'Chart Type' => 'Kontentyp', @@ -3184,6 +3189,7 @@ $self->{texts} = { 'Tax deleted!' => 'Steuer gelöscht!', 'Tax number' => 'Steuernummer', 'Tax paid' => 'Vorsteuer', + 'Tax point' => 'Leistungsdatum', 'Tax rate' => 'Steuersatz', 'Tax saved!' => 'Steuer gespeichert!', 'Tax zone' => 'Steuerzone', @@ -4124,6 +4130,7 @@ $self->{texts} = { 'brutto' => 'brutto', 'building data' => 'Verarbeite Daten', 'building report' => 'Erstelle Bericht', + 'can not allocate #1 units of #2, missing #3 units' => 'Kann keine #1 Einheiten von #2 belegen, es fehlen #3 Einheiten', 'can only parse a pdf file' => 'Kann nur eine gültige PDF-Datei verwenden.', 'cash' => 'Ist-Versteuerung', 'chargenumber #1' => 'Chargennummer #1', @@ -4272,6 +4279,7 @@ $self->{texts} = { 'our vendor number at customer' => 'Unsere Lieferanten-Nr. beim Kunden', 'parsing csv' => 'Parse CSV Daten', 'part' => 'Ware', + 'part \'#\'1 in bin \'#2\' only with qty #3 (need additional #4) and chargenumber \'#5\'.' => 'Artikel \'#1\' im \'#2\' nur mit der Menge #3 (noch #4 benötig) und Chargennummer \'#5\'.', 'part_list' => 'Warenliste', 'percental' => 'prozentual', 'periodic' => 'Aufwandsmethode', diff --git a/sql/Pg-upgrade2/record_template_payment_id.sql b/sql/Pg-upgrade2/record_template_payment_id.sql new file mode 100644 index 000000000..e251e3dd7 --- /dev/null +++ b/sql/Pg-upgrade2/record_template_payment_id.sql @@ -0,0 +1,5 @@ +-- @tag: record_template_payment_id +-- @description: Zahlungsbedingungen in Vorlagen in der Finanzbuchhaltung +-- @depends: release_3_5_6_1 + +ALTER TABLE record_templates ADD COLUMN payment_id INTEGER REFERENCES payment_terms(id); diff --git a/sql/Pg-upgrade2/tax_point.sql b/sql/Pg-upgrade2/tax_point.sql new file mode 100644 index 000000000..7fcb65d74 --- /dev/null +++ b/sql/Pg-upgrade2/tax_point.sql @@ -0,0 +1,7 @@ +-- @tag: tax_point +-- @description: Feld Leistungsdatum in Einkaufs- & Verkaufsbelegen +-- @depends: release_3_5_6_1 +ALTER TABLE ap ADD COLUMN tax_point DATE; +ALTER TABLE ar ADD COLUMN tax_point DATE; +ALTER TABLE gl ADD COLUMN tax_point DATE; +ALTER TABLE oe ADD COLUMN tax_point DATE; diff --git a/sql/Pg-upgrade2/tax_point2.sql b/sql/Pg-upgrade2/tax_point2.sql new file mode 100644 index 000000000..6caf13dff --- /dev/null +++ b/sql/Pg-upgrade2/tax_point2.sql @@ -0,0 +1,4 @@ +-- @tag: tax_point2 +-- @description: Feld Leistungsdatum in Lieferscheinen +-- @depends: tax_point +ALTER TABLE delivery_orders ADD COLUMN tax_point DATE; diff --git a/t/bank/bank_transactions.t b/t/bank/bank_transactions.t index e2bb55207..6ea6b687c 100644 --- a/t/bank/bank_transactions.t +++ b/t/bank/bank_transactions.t @@ -711,8 +711,8 @@ sub test_credit_note { transdate => $dt_10, ); my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($credit_note); - is($agreement, 13, "points for credit note ok"); - is($rule_matches, 'remote_account_number(3) exact_amount(4) wrong_sign(-1) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus14(2) ', "rules_matches for credit note ok"); + is($agreement, 14, "points for credit note ok"); + is($rule_matches, 'remote_account_number(3) exact_amount(4) depositor_matches(2) remote_name(2) payment_within_30_days(1) datebonus14(2) ', "rules_matches for credit note ok"); $::form->{invoice_ids} = { $bt->id => [ $credit_note->id ] @@ -764,7 +764,7 @@ sub test_neg_ap_transaction { ); my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($invoice); - is($agreement, 15, "points for negative ap transaction ok"); + is($agreement, 16, "points for negative ap transaction ok"); $::form->{invoice_ids} = { $bt->id => [ $invoice->id ] @@ -1227,4 +1227,36 @@ sub test_closedto { } +sub test_skonto_exact_ap_transaction { + + my $testname = 'test_skonto_exact_ap_transaction'; + + $ap_transaction = test_ap_transaction(invnumber => 'ap transaction skonto', + payment_id => $payment_terms->id, + ); + + my $bt = create_bank_transaction(record => $ap_transaction, + bank_chart_id => $bank->id, + transdate => $dt, + valutadate => $dt, + amount => $ap_transaction->amount_less_skonto + ) or die "Couldn't create bank_transaction"; + + $::form->{invoice_ids} = { + $bt->id => [ $ap_transaction->id ] + }; + $::form->{invoice_skontos} = { + $bt->id => [ 'with_skonto_pt' ] + }; + + save_btcontroller_to_string(); + + $ap_transaction->load; + $bt->load; + is($ap_transaction->paid , '119.00000' , "$testname: ap transaction skonto was paid"); + is($ap_transaction->closed , 1 , "$testname: ap transaction skonto is closed"); + is($bt->invoice_amount , '113.05000' , "$testname: bt invoice amount was assigned"); + +}; + 1; diff --git a/t/wh/inventory.t b/t/wh/inventory.t new file mode 100644 index 000000000..a10b9cf16 --- /dev/null +++ b/t/wh/inventory.t @@ -0,0 +1,285 @@ +use strict; +use Test::More; +use Test::Exception; + +use lib 't'; + +use SL::Dev::Part qw(new_part new_assembly); +use SL::Dev::Inventory qw(create_warehouse_and_bins set_stock); +use SL::Dev::Record qw(create_sales_order); + +use_ok 'Support::TestSetup'; +use_ok 'SL::DB::Bin'; +use_ok 'SL::DB::Part'; +use_ok 'SL::DB::Warehouse'; +use_ok 'SL::DB::Inventory'; +use_ok 'SL::WH'; +use_ok 'SL::Helper::Inventory'; + +Support::TestSetup::login(); + +my ($wh, $bin1, $bin2, $assembly1, $part1, $part2); + +reset_db(); +create_standard_stock(); + + +# simple stock in, get_stock, get_onhand +set_stock( + part => $part1, + qty => 25, + bin => $bin1, +); + +is(SL::Helper::Inventory::get_stock(part => $part1), "25.00000", 'simple get_stock works'); +is(SL::Helper::Inventory::get_onhand(part => $part1), "25.00000", 'simple get_onhand works'); + +# stock on some more, get_stock, get_onhand + +WH->transfer({ + parts_id => $part1->id, + qty => 15, + transfer_type => 'stock', + dst_warehouse_id => $bin1->warehouse_id, + dst_bin_id => $bin1->id, + comment => 'more', +}); + +WH->transfer({ + parts_id => $part1->id, + qty => 20, + transfer_type => 'stock', + chargenumber => '298345', + dst_warehouse_id => $bin1->warehouse_id, + dst_bin_id => $bin1->id, + comment => 'more', +}); + +is(SL::Helper::Inventory::get_stock(part => $part1), "60.00000", 'normal get_stock works'); +is(SL::Helper::Inventory::get_onhand(part => $part1), "60.00000", 'normal get_onhand works'); + +# allocate some stuff + +my @allocations = SL::Helper::Inventory::allocate( + part => $part1, + qty => 12, +); + +is_deeply(\%{ $allocations[0] }, { + bestbefore => undef, + bin_id => $bin1->id, + chargenumber => '', + parts_id => $part1->id, + qty => 12, + warehouse_id => $wh->id, + comment => undef, # comment is not a partition so is not set by allocate + for_object_id => undef, + }, 'allocation works'); + +# allocate something where more than one result will match + +@allocations = SL::Helper::Inventory::allocate( + part => $part1, + qty => 55, +); + +is_deeply(\@allocations, [ + { + bestbefore => undef, + bin_id => $bin1->id, + chargenumber => '', + parts_id => $part1->id, + qty => '40.00000', + warehouse_id => $wh->id, + comment => undef, + for_object_id => undef, + }, + { + bestbefore => undef, + bin_id => $bin1->id, + chargenumber => '298345', + parts_id => $part1->id, + qty => '15', + warehouse_id => $wh->id, + comment => undef, + for_object_id => undef, + } +], 'complex allocation works'); + +# try to allocate too much + +dies_ok(sub { + SL::Helper::Inventory::allocate(part => $part1, qty => 100) +}, +"allocate too much dies"); + +# produce something + +reset_db(); +create_standard_stock(); + +set_stock( + part => $part1, + qty => 5, + bin => $bin1, +); +set_stock( + part => $part2, + qty => 10, + bin => $bin1, +); + + +my @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 3); +my @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 3); + +SL::Helper::Inventory::produce_assembly( + part => $assembly1, + qty => 3, + allocations => [ @alloc1, @alloc2 ], + + # where to put it + bin => $bin1, + chargenumber => "537", +); + +is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce works'); +is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...'); +is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials'); + +# produce the same using auto_allocation + +reset_db(); +create_standard_stock(); + +set_stock( + part => $part1, + qty => 5, + bin => $bin1, +); +set_stock( + part => $part2, + qty => 10, + bin => $bin1, +); + +SL::Helper::Inventory::produce_assembly( + part => $assembly1, + qty => 3, + auto_allocate => 1, + + # where to put it + bin => $bin1, + chargenumber => "537", +); + +is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with auto allocation works'); +is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...'); +is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials'); + +# try to produce without allocations dies + +dies_ok(sub { +SL::Helper::Inventory::produce_assembly( + part => $assembly1, + qty => 3, + + # where to put it + bin => $bin1, + chargenumber => "537", +); +}, "producing without allocations dies"); + +# try to produce with insufficient allocations dies + +@alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 1); +@alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 1); + +dies_ok(sub { +SL::Helper::Inventory::produce_assembly( + part => $assembly1, + qty => 3, + allocations => [ @alloc1, @alloc2 ], + + # where to put it + bin => $bin1, + chargenumber => "537", +); +}, "producing with insufficient allocations dies"); + + + +# bestbefore tests + +reset_db(); +create_standard_stock(); + +set_stock( + part => $part1, + qty => 5, + bin => $bin1, +); +set_stock( + part => $part2, + qty => 10, + bin => $bin1, +); + + + +SL::Helper::Inventory::produce_assembly( + part => $assembly1, + qty => 3, + auto_allocate => 1, + + bin => $bin1, + chargenumber => "537", + bestbefore => DateTime->today->clone->add(days => -14), # expired 2 weeks ago + shippingdate => DateTime->today->clone->add(days => 1), +); + +is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with bestbefore works'); +is(SL::Helper::Inventory::get_onhand(part => $assembly1), "3.00000", 'produce with bestbefore works'); +is(SL::Helper::Inventory::get_stock( + part => $assembly1, + bestbefore => DateTime->today, +), undef, 'get_stock with bestbefore date skips expired'); +{ + local $::instance_conf->data->{show_bestbefore} = 1; + is(SL::Helper::Inventory::get_onhand( + part => $assembly1, + ), undef, 'get_onhand with bestbefore skips expired as of today'); +} + +{ + local $::instance_conf->data->{show_bestbefore} = 0; + is(SL::Helper::Inventory::get_onhand( + part => $assembly1, + ), "3.00000", 'get_onhand without bestbefore finds all'); +} + + +sub reset_db { + SL::DB::Manager::Order->delete_all(all => 1); + SL::DB::Manager::Inventory->delete_all(all => 1); + SL::DB::Manager::Assembly->delete_all(all => 1); + SL::DB::Manager::Part->delete_all(all => 1); + SL::DB::Manager::Bin->delete_all(all => 1); + SL::DB::Manager::Warehouse->delete_all(all => 1); +} + +sub create_standard_stock { + ($wh, $bin1) = create_warehouse_and_bins(); + $bin2 = SL::DB::Bin->new(description => "Bin 2", warehouse => $wh)->save; + $wh->load; + + $assembly1 = new_assembly(number_of_parts => 2)->save; + ($part1, $part2) = map { $_->part } $assembly1->assemblies; +} + + +reset_db(); + +done_testing(); + +1; diff --git a/templates/print/marei/kiviletter.sty b/templates/print/marei/kiviletter.sty index abbc1a101..90ccaff51 100644 --- a/templates/print/marei/kiviletter.sty +++ b/templates/print/marei/kiviletter.sty @@ -162,7 +162,7 @@ contents={\usebox\shippingAddressBox} \prg_new_conditional:Nnn \kivi_if_Price_col:n {T} { \prop_get:cnN {l_kivi_col_#1_prop} {colspec} \l_tmpa_tl - \tl_if_eq:NnTF \l_tmpa_tl {Price} + \exp_args:NV \tl_if_eq:nnTF \l_tmpa_tl {Price} {\prg_return_true:} {\prg_return_false:} } @@ -191,7 +191,7 @@ contents={\usebox\shippingAddressBox} \dim_use:c {l_kivi_tab_##1_dim}+2\g_kivi_tabcolsep_dim } } - \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {>{\raggedleft\arraybackslash}p{\dim_use:c {l_kivi_tab_##1_dim}}} + \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {K{\dim_use:c {l_kivi_tab_##1_dim}}} \kivi_if_Price_col:nT {##1} {\tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {<{\__kivi_tab_column_currency:}}} } } @@ -200,7 +200,8 @@ contents={\usebox\shippingAddressBox} \tl_gput_right:Nn \g_kivi_Pricing_colspec_tl {@{}} } -\newcolumntype{P}[1]{>{\raggedleft\arraybackslash}p{#1}<{\__kivi_tab_column_currency:}} +\newcolumntype{K}[1]{>{\raggedleft\arraybackslash}p{#1}} +\newcolumntype{P}[1]{K{#1}<{\__kivi_tab_column_currency:}} \RequirePackage{tcolorbox} \tcbuselibrary{breakable, skins} diff --git a/templates/webpages/ap/form_header.html b/templates/webpages/ap/form_header.html index 6f73fd9d5..2f22a96f2 100644 --- a/templates/webpages/ap/form_header.html +++ b/templates/webpages/ap/form_header.html @@ -163,6 +163,10 @@ [% 'Due Date' | $T8 %] [% L.date_tag('duedate', duedate) %] + + [% LxERP.t8('Tax point') %] + [% L.date_tag('tax_point', tax_point, id='tax_point') %] + [% 'Delivery Date' | $T8 %] [% L.date_tag('deliverydate', deliverydate) %] @@ -234,19 +238,24 @@ - - - - - - - - -
[% 'Notes' | $T8 %] - - [% 'Notes for vendor' | $T8 %] - -
+ + + + + + + + + + + +
[% 'Notes' | $T8 %][% 'Internal Notes' | $T8 %][% 'Payment Terms' | $T8 %]
+ [% L.textarea_tag("notes", notes, wrap="soft", rows=textarea_rows, cols=50, readonly=readonly) %] + + [% L.textarea_tag("intnotes", intnotes, wrap="soft", rows=textarea_rows, cols=50, readonly=readonly) %] + + [% L.select_tag('payment_id', payment_terms, default=payment_id, title_key='description', with_empty=1, style="width: 250px", onchange="kivi.SalesPurchase.set_duedate_on_reference_date_change('invdate')") %] +
diff --git a/templates/webpages/ar/form_header.html b/templates/webpages/ar/form_header.html index c1908a418..755d564c0 100644 --- a/templates/webpages/ar/form_header.html +++ b/templates/webpages/ar/form_header.html @@ -141,6 +141,10 @@ [% 'Due Date' | $T8 %] [% L.date_tag('duedate', duedate) %] + + [% LxERP.t8('Tax point') %] + [% L.date_tag('tax_point', tax_point, id='tax_point') %] + [% 'Delivery Date' | $T8 %] [% L.date_tag('deliverydate', deliverydate) %] diff --git a/templates/webpages/do/form_header.html b/templates/webpages/do/form_header.html index 1cac15be3..9ff8e98a1 100644 --- a/templates/webpages/do/form_header.html +++ b/templates/webpages/do/form_header.html @@ -120,6 +120,7 @@ +

diff --git a/templates/webpages/gl/form_header.html b/templates/webpages/gl/form_header.html index b021909bd..ba9036c8d 100644 --- a/templates/webpages/gl/form_header.html +++ b/templates/webpages/gl/form_header.html @@ -80,21 +80,21 @@ - - + + - - + + -[%- IF id %] - - + + + + -[%- END %] + + + + diff --git a/templates/webpages/is/form_header.html b/templates/webpages/is/form_header.html index e30974023..c23c16229 100644 --- a/templates/webpages/is/form_header.html +++ b/templates/webpages/is/form_header.html @@ -238,8 +238,14 @@ +[%- END %] + + + + +[%- IF !is_type_credit_note %] - + [%- END %] diff --git a/templates/webpages/oe/form_header.html b/templates/webpages/oe/form_header.html index 556987417..2390718f3 100644 --- a/templates/webpages/oe/form_header.html +++ b/templates/webpages/oe/form_header.html @@ -225,6 +225,10 @@ [% L.date_tag('transdate', transdate, id='transdate') %] + + + + + + + + + [%- IF (SELF.type == "sales_order" || SELF.type == "purchase_order") -%] - [%- SET reqdate_txt = 'Reqdate' -%] + [%- SET reqdate_txt = 'Reqdate'; SET reqdate_class = 'recalc' -%] [%- ELSIF SELF.type == "sales_quotation" -%] - [%- SET reqdate_txt = 'Valid until' -%] + [%- SET reqdate_txt = 'Valid until'; SET reqdate_class = '' -%] [%- ELSE -%] - [%- SET reqdate_txt = 'Required by' -%] + [%- SET reqdate_txt = 'Required by'; SET reqdate_class = 'recalc' -%] [%- END -%] - + [%- IF SELF.type == "sales_quotation" -%] @@ -205,7 +210,7 @@ - + [%- END %]
[% 'Department' | $T8 %] [% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1) %][% 'Delivery Date' | $T8 %][% L.date_tag('deliverydate', deliverydate) %][% 'Tax point' | $T8 %][% L.date_tag('tax_point', tax_point) %]
[% 'Description' | $T8 %] [% L.areainput_tag('description', description, cols=50, readonly=readonly) %][% 'MwSt. inkl.' | $T8 %][% L.checkbox_tag('taxincluded', checked=taxincluded) %][% 'Delivery Date' | $T8 %][% L.date_tag('deliverydate', deliverydate) %]
[% 'Mitarbeiter' | $T8 %][% L.input_tag('employee', employee, size=20, readonly=readonly) %][%- IF id %][% 'Mitarbeiter' | $T8 %][% END %][%- IF id %][% L.input_tag('employee', employee, size=20, readonly=readonly) %][% END %][% 'MwSt. inkl.' | $T8 %][% L.checkbox_tag('taxincluded', checked=taxincluded) %]
diff --git a/templates/webpages/inventory/stocktaking/form.html b/templates/webpages/inventory/stocktaking/form.html index 0f85da304..ffe3600b4 100644 --- a/templates/webpages/inventory/stocktaking/form.html +++ b/templates/webpages/inventory/stocktaking/form.html @@ -12,7 +12,7 @@

- [% P.part.picker("part_id", "") %] + [% P.part.picker("part_id", "", with_makemodel=1) %]

diff --git a/templates/webpages/ir/form_header.html b/templates/webpages/ir/form_header.html index 1c652d893..fac160a03 100644 --- a/templates/webpages/ir/form_header.html +++ b/templates/webpages/ir/form_header.html @@ -154,6 +154,10 @@

[% LxERP.t8('Tax point') %][% L.date_tag('tax_point', tax_point, id='tax_point') %]
[% 'Delivery Date' | $T8 %] [% L.date_tag('deliverydate', deliverydate) %]
[% LxERP.t8('Tax point') %][% L.date_tag('tax_point', tax_point, id='tax_point') %]
[% 'Delivery Order Number' | $T8 %][% 'Delivery Order Number' | $T8 %]
[% LxERP.t8('Tax point') %][% L.date_tag('tax_point', tax_point, id='tax_point') %]
[%- IF is_sales_quo %] diff --git a/templates/webpages/order/tabs/basic_data.html b/templates/webpages/order/tabs/basic_data.html index d7b84ff88..05a663c1c 100644 --- a/templates/webpages/order/tabs/basic_data.html +++ b/templates/webpages/order/tabs/basic_data.html @@ -186,16 +186,21 @@ [% L.date_tag('order.transdate_as_date', SELF.order.transdate_as_date) %]
[% 'Tax point' | $T8 %][% L.date_tag('order.tax_point_as_date', SELF.order.tax_point_as_date, class="recalc") %]
[% reqdate_txt | $T8 %][% L.date_tag('order.reqdate_as_date', SELF.order.reqdate_as_date) %][% L.date_tag('order.reqdate_as_date', SELF.order.reqdate_as_date, class=reqdate_class) %]
[% 'Expected billing date' | $T8 %][%- L.date_tag('order.expected_billing_date_as_date', SELF.order.expected_billing_date_sa_date) %][%- L.date_tag('order.expected_billing_date_as_date', SELF.order.expected_billing_date_as_date) %]