Brieffunktion erste Version
authorJan Büren <jan@kivitendo-premium.de>
Wed, 11 Mar 2015 11:58:57 +0000 (12:58 +0100)
committerJan Büren <jan@kivitendo-premium.de>
Wed, 11 Mar 2015 11:58:57 +0000 (12:58 +0100)
Verkaufsbriefe können jetzt auch mit LaTeX erstellt werden.
Ferner gibt es eine Briefentwurfs-Funktion, die aussieht wie drafts, aber
zumindestens auf einer eigenen Tabelle beruht. Zusätzlich wurden zwei neue Rechte
gesetzt

18 files changed:
SL/Auth.pm
SL/Form.pm
SL/Letter.pm [new file with mode: 0644]
SL/TransNumber.pm
bin/mozilla/letter.pl [new file with mode: 0755]
locale/de/all
menus/erp.ini
sql/Pg-upgrade2-auth/sales_letter_rights.pl [new file with mode: 0644]
sql/Pg-upgrade2/letter.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_country_page.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_cp_id.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_date_type.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_draft.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_employee_salesman.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_notes_internal.sql [new file with mode: 0644]
sql/Pg-upgrade2/letter_reference.sql [new file with mode: 0644]
templates/print/RB/letter.tex [new file with mode: 0644]
templates/print/Standard/letter.tex [new file with mode: 0644]

index a2236a7..8439a2c 100644 (file)
@@ -957,11 +957,13 @@ sub all_rights_full {
     ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
     ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
     ["dunning_edit",                   $locale->text("Create and edit dunnings")],
+    ["sales_letter_edit",              $locale->text("Edit sales letters")],
     ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
     ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
     ["show_ar_transactions",           $locale->text("Show AR transactions as part of AR invoice report")],
     ["delivery_plan",                  $locale->text("Show delivery plan")],
     ["delivery_value_report",          $locale->text("Show delivery value report")],
+    ["sales_letter_report",            $locale->text("Show sales letters report")],
     ["--ap",                           $locale->text("AP")],
     ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
     ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
index acc5cff..0aad56e 100644 (file)
@@ -1235,6 +1235,7 @@ sub get_formname_translation {
     sales_delivery_order    => $main::locale->text('Delivery Order'),
     purchase_delivery_order => $main::locale->text('Delivery Order'),
     dunning                 => $main::locale->text('Dunning'),
+    letter                  => $main::locale->text('Letter')
   );
 
   $main::lxdebug->leave_sub();
@@ -1249,8 +1250,13 @@ sub get_number_prefix_for_type {
       (first { $self->{type} eq $_ } qw(invoice credit_note)) ? 'inv'
     : ($self->{type} =~ /_quotation$/)                        ? 'quo'
     : ($self->{type} =~ /_delivery_order$/)                   ? 'do'
+    : ($self->{type} =~ /letter/)                             ? 'letter'
     :                                                           'ord';
 
+  # better default like this?
+  # : ($self->{type} =~ /(sales|purcharse)_order/           :  'ord';
+  # :                                                           'prefix_undefined';
+
   $main::lxdebug->leave_sub();
   return $prefix;
 }
@@ -2387,7 +2393,7 @@ sub get_lists {
   if ($params{contacts} || $params{shipto}) {
     $vc = 'customer' if $self->{"vc"} eq "customer";
     $vc = 'vendor'   if $self->{"vc"} eq "vendor";
-    die "invalid use of get_lists, need 'vc'";
+    die "invalid use of get_lists, need 'vc'" unless $vc;
     $vc_id = $self->{"${vc}_id"};
   }
 
diff --git a/SL/Letter.pm b/SL/Letter.pm
new file mode 100644 (file)
index 0000000..1d1b974
--- /dev/null
@@ -0,0 +1,404 @@
+#=====================================================================
+# LX-Office ERP
+# Copyright (C) 2008
+# Based on SQL-Ledger Version 2.1.9
+# Web http://www.lx-office.org
+#
+#=====================================================================
+#
+# Letter module
+#
+#=====================================================================
+
+package SL::Letter;
+
+use strict;
+use List::Util qw(max);
+
+use SL::Common;
+use SL::CT;
+use SL::DBUtils;
+use SL::MoreCommon;
+use SL::TransNumber;
+use SL::DB::Manager::Customer;
+
+my $DEFINITION = <<SQL;
+                                      Table "public.letter"
+        Column       |            Type             |                Modifiers
+  -------------------+-----------------------------+------------------------------------------
+   id                | integer                     | not null default nextval('id'::regclass)
+   vc_id             | integer                     | not null
+   letternumber      | text                        |
+   jobnumber         | text                        |
+   text_created_for  | text                        |
+   date              | date                        |
+   subject           | text                        |
+   greeting          | text                        |
+   body              | text                        |
+   close             | text                        |
+   company_name      | text                        |
+   employee_id       | integer                     |
+   employee_position | text                        |
+   salesman_id       | integer                     |
+   salesman_position | text                        |
+   itime             | timestamp without time zone | default now()
+   mtime             | timestamp without time zone |
+   page_created_for  | text                        |
+   intnotes          | text                        |
+   cp_id             | integer                     |
+   reference         | text                        |
+  Indexes:
+      "letter_pkey" PRIMARY KEY, btree (id)
+  Foreign-key constraints:
+      "letter_cp_id_fkey" FOREIGN KEY (cp_id) REFERENCES contacts(cp_id)
+      "letter_employee_id_fkey" FOREIGN KEY (employee_id) REFERENCES employee(id)
+      "letter_salesman_id_fkey" FOREIGN KEY (salesman_id) REFERENCES employee(id)
+SQL
+
+# XXX not working yet
+#sub customer {
+#  my $self = shift;
+#
+#  die 'not a setter' if @_;
+#
+#  return unless $self->{customer_id};
+#
+#  # resolve customer_obj
+#}
+
+sub new {
+  my $class  = ref $_[0] || $_[0]; shift;
+  my %params = @_;
+  my $ref    = $_[0];
+
+  $ref = ref $_[0] eq 'HASH' ? $ref : \%params; # don't like it either...
+
+  my $self = bless $ref, $class;
+
+  $self->_lastname_used;
+  $self->_resolve_customer;
+  $self->set_greetings;
+
+  return $self;
+}
+
+sub _create {
+  my $self = shift;
+  my $dbh  = $::form->get_standard_dbh;
+  ($self->{id}) = selectfirst_array_query($::form, $dbh, "select nextval('id')");
+
+  do_query($::form, $dbh, <<SQL, $self->{id}, $self->{customer_id});
+    INSERT INTO letter (id, vc_id) VALUES (?, ?);
+SQL
+}
+
+sub _create_draft {
+  my $self = shift;
+  my $dbh  = $::form->get_standard_dbh;
+  ($self->{draft_id}) = selectfirst_array_query($::form, $dbh, "select nextval('id')");
+
+  do_query($::form, $dbh, <<SQL, $self->{draft_id}, $self->{customer_id});
+    INSERT INTO letter_draft (id, vc_id) VALUES (?, ?);
+SQL
+}
+
+
+sub save {
+  $::lxdebug->enter_sub;
+
+  my $self     = shift;
+  my %params   = @_;
+  my $dbh      = $::form->get_standard_dbh;
+  my ($table, $update_value);
+
+  if ($params{draft}) {
+    $self->_create_draft unless $self->{draft_id};
+    $table = 'letter_draft';
+    $update_value = 'draft_id';
+  } else {
+    $self->_create unless $self->{id};
+    $table = 'letter';
+    $update_value = 'id';
+  }
+
+  my %fields         = __PACKAGE__->_get_fields;
+  my %field_mappings = __PACKAGE__->_get_field_mappings;
+
+  delete $fields{id};
+
+  my @update_fields = keys %fields;
+  my $set_clause    = join ', ', map { "$_ = ?" } @update_fields;
+  my @values        = map { _escaper($_)->( $self->{ $field_mappings{$_} || $_ } ) } @update_fields, $update_value;
+
+  my $query = "UPDATE $table SET $set_clause WHERE id = ?";
+
+  do_query($::form, $dbh, $query, @values);
+
+  $dbh->commit;
+
+  $::lxdebug->leave_sub;
+}
+
+sub find {
+  $::lxdebug->enter_sub;
+
+  my $class    = ref $_[0] || $_[0]; shift;
+  my $myconfig = \%main::myconfig;
+  my $form     = $main::form;
+  my $dbh      = $form->get_standard_dbh($myconfig);
+  my %params   = @_;
+  my $letter_table = 'letter';
+
+  $letter_table = 'letter_draft' if $params{draft};
+  %params = %$form if  !scalar keys %params;
+
+  my (@wheres, @values);
+  my $add_token = sub { add_token(\@wheres, \@values, @_) };
+
+  $add_token->(col => 'letter.id',           val => $params{id},           esc => 'id'    ) if $params{id};
+  $add_token->(col => 'letter.letternumber', val => $params{letternumber}, esc => 'substr') if $params{letternumber};
+  $add_token->(col => 'vc.name',             val => $params{customer},     esc => 'substr') if $params{customer};
+  $add_token->(col => 'vc.id',               val => $params{customer_id},  esc => 'id'    ) if $params{customer_id};
+  $add_token->(col => 'letter.cp_id',        val => $params{cp_id},        esc => 'id'    ) if $params{cp_id};
+  $add_token->(col => 'ct.cp_name',          val => $params{contact},      esc => 'substr') if $params{contact};
+  $add_token->(col => 'letter.subject',      val => $params{subject},      esc => 'substr') if $params{subject};
+  $add_token->(col => 'letter.body',         val => $params{body},         esc => 'substr') if $params{body};
+  $add_token->(col => 'letter.date',         val => $params{date_from}, method => '>='    ) if $params{date_from};
+  $add_token->(col => 'letter.date',         val => $params{date_to},   method => '<='    ) if $params{date_to};
+
+  my $query = qq|
+    SELECT $letter_table.*, vc.name AS customer, vc.id AS customer_id, ct.cp_name AS contact FROM $letter_table
+      LEFT JOIN customer vc ON vc.id = $letter_table.vc_id
+      LEFT JOIN contacts ct ON $letter_table.cp_id = ct.cp_id
+  |;
+
+  if (@wheres) {
+    $query .= ' WHERE ' . join ' AND ', @wheres;
+  }
+
+  my @results = selectall_hashref_query($form, $dbh, $query, @values);
+  my @objects = map { $class->new($_) } @results;
+
+  $::lxdebug->leave_sub;
+
+  return @objects;
+}
+
+sub delete {
+  $::lxdebug->enter_sub;
+
+  my $self     = shift;
+
+  do_query($::form, $::form->get_standard_dbh, <<SQL, $self->{id});
+    DELETE FROM letter WHERE id = ?
+SQL
+
+  $::form->get_standard_dbh->commit;
+
+  $::lxdebug->leave_sub;
+}
+
+sub delete_drafts {
+  $::lxdebug->enter_sub;
+
+  my $self        = shift;
+  my @draft_ids   = @_;
+
+  my $form        = $main::form;
+  my $myconfig = \%main::myconfig;
+  my $dbh         = $form->get_standard_dbh($myconfig);
+
+
+  return $main::lxdebug->leave_sub() unless (@draft_ids);
+
+  my  $query = qq|DELETE FROM letter_draft WHERE id IN (| . join(", ", map { "?" } @draft_ids) . qq|)|;
+  do_query($form, $dbh, $query, @draft_ids);
+
+  $dbh->commit;
+
+  $::lxdebug->leave_sub;
+}
+
+
+sub check_number {
+  my $self = shift;
+
+  return if $self->{letternumber}
+         && $self->{id}
+         && 1 == scalar __PACKAGE__->find(letternumber => $self->{letternumber});
+
+  $self->{letternumber} = SL::TransNumber->new(type => 'letter', id => $self->{id}, number => $self->{letternumber})->create_unique;
+}
+
+sub check_name {
+  my $self   = shift;
+  my %params = @_;
+
+  unless ($params{_name_selected}) {
+    $::form->{$_} = $self->{$_} for qw(oldcustomer customer selectcustomer customer_id);
+
+    if (::check_name('customer')) {
+      $self->_set_customer_from($::form);
+    }
+  } else {
+    $self->_set_customer_from($::form);
+  }
+}
+
+sub _set_customer_from {
+  my $self = shift;
+  my $from = shift;
+
+  $self->{$_} = $from->{$_} for qw(oldcustomer customer_id customer selectcustomer);
+
+  $self;
+}
+
+sub check_date {
+  my $self = shift;
+  $self->{date} ||= $::form->current_date(\%::myconfig);
+}
+
+sub load {
+  my $self   = shift;
+  my $table  = 'letter';
+  my $draft = $self->{draft};
+  $table     = 'letter_draft' if $draft;
+
+
+  return $self unless $self && $self->{id}; # no id? dont load.
+
+  my %mappings      = _get_field_mappings();
+  my $mapped_select = join ', ', '*', map { "$_ AS $mappings{$_}" } keys %mappings;
+
+  my ($db_letter) = selectfirst_hashref_query($::form, $::form->get_standard_dbh, <<SQL, $self->{id});
+    SELECT $mapped_select FROM $table WHERE id = ?
+SQL
+
+  $self->update_from($db_letter);
+  $self->_resolve_customer;
+  $self->set_greetings;
+  $self->{draft_id} = delete $self->{id} if $draft;  # set draft if we have one
+
+  return $self;
+}
+
+sub update_from {
+  my $self   = shift;
+  my $src    = shift;
+  my %fields = $self->_get_fields;
+
+  $fields{$_} = $src->{$_} for qw{customer_id customer selectcustomer oldcustomer}; # customer stuff
+
+  $self->{$_} = $src->{$_} for keys %fields;
+
+  return $self;
+}
+
+sub export_to {
+  my $self = shift;
+  my $form = shift;
+
+  my %fields         = $self->_get_fields;
+  my %field_mappings = $self->_get_field_mappings;
+
+  for (keys %fields) {
+    $form->{$_} =  _escaper($_)->( $self->{ $field_mappings{$_} || $_ } );
+  }
+}
+
+sub language {
+  my $self = shift;
+  die 'not a setter' if @_;
+
+  return unless $self->{cp_id};
+
+  # umetec/cetaq only!
+  # contacts have a custom variable called "mailing"
+  # it contains either a language code or the string "No"
+
+  my $custom_variables = CVar->get_custom_variables(
+    module      => 'Contacts',
+    name_prefix => 'cp',
+    trans_id    => $self->{cp_id},
+  );
+
+  my ($mailing) = grep { $_->{name} eq 'Mailing' } @$custom_variables;
+
+  return $mailing->{value} eq 'No' ? undef : $mailing->{value};
+}
+
+sub set_greetings {
+  $::lxdebug->enter_sub;
+
+  my $self = shift;
+  return $::lxdebug->leave_sub if $self->{greeting};
+
+  # automatically set greetings
+  # greetings depend mainly on contact person
+#   my $contact = $self->_get_contact;
+
+  $self->{greeting} = $::locale->text('Dear Sir or Madam,');
+
+  $::lxdebug->leave_sub;
+}
+
+sub _lastname_used {
+  # wrapper for form lastname_used
+  # sets customer to last used customer,
+  # also used to initalize customer for new objects
+  my $self = shift;
+
+  return if $self->{customer_id};
+
+  my $saved_form = save_form($::form);
+
+  $::form->lastname_used($::form->get_standard_dbh, \%::myconfig, 'customer');
+
+  $self->{customer_id} = $::form->{customer_id};
+  $self->{customer}    = $::form->{customer};
+
+  restore_form($saved_form);
+
+  return $self;
+}
+
+sub _resolve_customer {
+  # used if an object is created with only id.
+  my $self = shift;
+
+  return unless $self->{customer_id} && !$self->{customer};
+
+#  my ($customer) = CT->find_by_id(cv => 'customer', id => $self->{customer_id});
+#  my ($customer) = CT->find_by_id(cv => 'customer', id => $self->{customer_id});
+  # SL/CVar.pm:        : $cfg->{type} eq 'customer'  ? (SL::DB::Manager::Customer->find_by(id => 1*$ref->{number_value}) || SL::DB::Customer->new)->name
+  $self->{customer} = SL::DB::Manager::Customer->find_by(id => $self->{customer_id})->name; # || SL::DB::Customer->new)->name
+
+
+}
+
+sub _get_definition {
+  $DEFINITION;
+}
+
+sub _get_field_mappings {
+  return (
+    vc_id => 'customer_id',
+  );
+}
+
+sub _get_fields {
+  my %fields = _get_definition() =~ /(\w+) \s+ \| \s+ (integer|text|timestamp|numeric|date)/xg;
+}
+
+sub _escaper {
+  my $field_name = shift;
+  my %fields     = __PACKAGE__->_get_fields;
+
+  for ($fields{$field_name}) {
+    return sub { conv_i(shift) } if /integer/;
+    return sub { shift };
+  }
+}
+
+1;
index fb715b8..8be5bcd 100644 (file)
@@ -14,7 +14,7 @@ use Rose::Object::MakeMethods::Generic
  scalar => [ qw(type id number save dbh dbh_provided business_id) ],
 );
 
-my @SUPPORTED_TYPES = qw(invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly);
+my @SUPPORTED_TYPES = qw(invoice credit_note customer vendor sales_delivery_order purchase_delivery_order sales_order purchase_order sales_quotation request_quotation part service assembly letter);
 
 sub new {
   my $class = shift;
@@ -71,6 +71,10 @@ sub _get_filters {
     $filters{numberfield}   = $type eq 'service' ? 'servicenumber' : 'articlenumber';
     $filters{numberfield}   = $type eq 'assembly' ? 'assemblynumber' : $filters{numberfield};
     $filters{table}         = "parts";
+  } elsif ($type =~ /letter/) {
+    $filters{trans_number}  = "letternumber";
+    $filters{numberfield}   = "letternumber";
+    $filters{table}         = "letter";
   }
 
   return %filters;
diff --git a/bin/mozilla/letter.pl b/bin/mozilla/letter.pl
new file mode 100755 (executable)
index 0000000..18564d3
--- /dev/null
@@ -0,0 +1,552 @@
+#=====================================================================
+# LX-Office ERP
+# Copyright (C) 2008
+# Based on SQL-Ledger Version 2.1.9
+# Web http://www.lx-office.org
+#
+#=====================================================================
+#
+# Letter module
+#
+#======================================================================
+
+use strict;
+use POSIX qw(strftime);
+
+use SL::GenericTranslations;
+use SL::ReportGenerator;
+use SL::Letter;
+use SL::CT;
+use SL::DB::Contact;
+use SL::DB::Default;
+use SL::Helper::CreatePDF;
+use SL::Helper::Flash;
+require "bin/mozilla/reportgenerator.pl";
+require "bin/mozilla/io.pl";
+require "bin/mozilla/arap.pl";
+
+use constant TEXT_CREATED_FOR_VALUES => (qw(presskit fax letter));
+use constant PAGE_CREATED_FOR_VALUES => (qw(sketch 1 2));
+
+our ($form, %myconfig, $locale, $lxdebug);
+
+# parserhappy(R)
+# $locale->text('Presskit')
+# $locale->text('Sketch')
+# $locale->text('Fax')
+# $locale->text('Letter')
+
+sub add {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+  my %params = @_;
+
+  return $main::lxdebug->leave_sub if load_letter_draft();
+
+  my $letter = SL::Letter->new(%params);
+
+  if (my $cp_id = delete $::form->{contact_id}) {
+    my $contact = SL::DB::Manager::Contact->find_by(cp_id => $cp_id);
+    $letter->{cp_id}     = $contact->cp_id;
+    $letter->{vc_id}     = $contact->cp_cv_id;
+    $letter->{greeting}  = GenericTranslations->get(
+      translation_type => 'greetings::' . ($contact->{cp_gender} eq 'f' ? 'female' : 'male'),
+      language_id      => $contact->language_id,
+      allow_fallback   => 1
+    );
+    $params{language_id} = $contact->language_id;
+  }
+
+  $letter->check_date;
+
+  _display(
+    letter      => $letter,
+    title       => $locale->text('Add Letter'),
+    language_id => $params{language_id},
+  );
+
+  $::lxdebug->leave_sub;
+}
+
+sub edit {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+  add() unless ($form->{id});
+
+  my $letter = SL::Letter->new( id => $form->{id}, draft => $form->{draft} )->load;
+
+  add() unless $letter && ($letter->{id} || $letter->{draft_id});
+
+  _display(
+    letter => $letter,
+    title  => $locale->text('Edit Letter'),
+  );
+
+  $::lxdebug->leave_sub;
+}
+
+sub save {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+  my %params = @_;
+
+
+  $::form->error(t8('The subject is missing.')) unless $form->{letter}->{subject};
+  $::form->error(t8('The body is missing.')) unless $form->{letter}->{body};
+  $::form->error(t8('The employee is missing.')) unless $form->{letter}->{employee_id};
+
+  my $letter = _update();
+
+  $letter->check_number;
+  $letter->save;
+
+  $form->{SAVED_MESSAGE} = $locale->text('Letter saved!');
+
+  _display(
+    letter => $letter,
+  );
+
+  $::lxdebug->leave_sub;
+}
+
+sub save_letter_draft {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+
+  $::form->error(t8('The subject is missing.')) unless $form->{letter}->{subject};
+  $::form->error(t8('The body is missing.')) unless $form->{letter}->{body};
+  $::form->error(t8('The employee is missing.')) unless $form->{letter}->{employee_id};
+  $::form->error(t8('Already as letter saved.')) if $form->{letter}->{letternumber};
+
+  my $letter_draft = _update();
+  $letter_draft->{draft_id} = delete $letter_draft->{id}; # if we have one
+  $letter_draft->save(draft => '1');
+  $letter_draft->{vergiss_mich_nicht} = 'nicht vergessen';
+  $form->{SAVED_MESSAGE} = $locale->text('Draft for this Letter saved!');
+
+  _display(
+    letter => $letter_draft,
+  );
+
+  $::lxdebug->leave_sub;
+}
+
+sub delete {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_edit');
+  # NYI
+  $form->{SAVED_MESSAGE} = $locale->text('Not yet implemented!');
+  _display();
+
+  $main::lxdebug->leave_sub();
+}
+
+sub delete_letter_drafts {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_edit');
+
+  my @ids;
+  foreach (keys %{$form}) {
+    push @ids, $1 if (/^checked_(.*)/ && $form->{$_});
+  }
+
+  SL::Letter->delete_drafts(@ids) if (@ids); #->{id});
+
+  add();
+
+  $main::lxdebug->leave_sub();
+}
+
+sub _display {
+  $main::lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_edit');
+  my %params = @_;
+
+  my $letter = $params{letter};
+
+  my %TMPL_VAR;
+
+  $form->{type}             = 'letter';   # needed for print_options
+  $form->{vc}               = 'customer'; # needs to be for _get_contacts...
+  $form->{"$form->{vc}_id"} ||= $letter->{customer_id};
+  $form->{jsscript}         = 1;
+  $form->{javascript}       =
+     qq|<script type="text/javascript" src="js/customer_or_vendor_selection.js"></script>
+        <script type="text/javascript" src="js/edit_part_window.js"></script>|;
+
+  $form->get_lists("contacts"      => "ALL_CONTACTS",
+  "employees"     => "ALL_EMPLOYEES",
+                   "salesmen"      => "ALL_SALESMEN",
+                   "departments"   => "ALL_DEPARTMENTS",
+                   "languages"     => "languages",
+                   "customers"     => { key   => "ALL_CUSTOMERS",
+                                        limit => $myconfig{vclimit} + 1 },
+                   "vc"            => 'customer',
+                   );
+
+  $TMPL_VAR{vc_keys}       = sub { "$_[0]->{name}--$_[0]->{id}" };
+  $TMPL_VAR{vc_select}     = "customer_or_vendor_selection_window('letter.customer', '', 0, 0)";
+  $TMPL_VAR{ct_labels}     = sub { ($_[0]->{cp_greeting} ? "$_[0]->{cp_greeting} " : '') .  $_[0]->{cp_name} .  ($_[0]->{cp_givenname} ? ", $_[0]->{cp_givenname}" : '') };
+  $TMPL_VAR{TCF}           = [ map { key => $_, value => $locale->text(ucfirst $_) }, TEXT_CREATED_FOR_VALUES() ];
+  $TMPL_VAR{PCF}           = [ map { key => $_, value => $locale->text(ucfirst $_) }, PAGE_CREATED_FOR_VALUES() ];
+
+  $form->header();
+
+  $form->{language_id} ||= $params{language_id};
+
+  print $form->parse_html_template('letter/edit', {
+    %params,
+    %TMPL_VAR,
+    letter        => $letter,
+    print_options => print_options(inline => 1),
+  });
+
+  $main::lxdebug->leave_sub();
+}
+
+sub search {
+  $lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_report');
+
+  $form->get_lists("employees" => "EMPLOYEES",
+                   "salesmen"  => "SALESMEN",
+                   "customers" => "ALL_CUSTOMERS");
+
+  $form->{jsscript} = 1;
+  $form->{title}    = $locale->text('Letters');
+
+  $form->header();
+  print $form->parse_html_template('letter/search');
+
+  $lxdebug->leave_sub();
+}
+
+sub report {
+  $lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_report');
+
+  my %params = @_;
+
+  my @report_params = qw(letternumber subject body contact date_from date_to cp_id);
+
+  if ($form->{selectcustomer}) {
+    push @report_params, 'customer_id';
+    $form->{customer_id} = $form->{customer};
+  } else {
+    push @report_params, 'customer';
+  }
+
+  report_generator_set_default_sort('date', 1);
+
+  %params = (%params, map { $_ => $form->{$_} } @report_params);
+
+  my @letters       = SL::Letter->find(%params);
+
+  $form->{rowcount} = @letters;
+  $form->{title}    = $locale->text('Letters');
+
+  my %column_defs = (
+    'date'                  => { 'text' => $locale->text('Date'), },
+    'subject'               => { 'text' => $locale->text('Subject'), },
+    'letternumber'          => { 'text' => $locale->text('Letternumber'), },
+    'customer'              => { 'text' => $locale->text('Customer') },
+    'contact'               => { 'text' => $locale->text('Contact') },
+    'date'                  => { 'text' => $locale->text('Date') },
+  );
+
+  my @columns = qw(date subject letternumber customer contact date);
+  my $href    = build_std_url('action=report', grep { $form->{$_} } @report_params);
+
+  my @sortable_columns = qw(date subject letternumber customer contact date);
+
+  foreach my $name (@sortable_columns) {
+    my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
+    $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
+  }
+
+  my @options;
+
+  # option line
+
+  push @options, $locale->text('Subject')                  . " : $form->{subject}"   if ($form->{subject});
+  push @options, $locale->text('Body')                     . " : $form->{body}"      if ($form->{body});
+
+  my @hidden_report_params = map { +{ 'key' => $_, 'value' => $form->{$_} } } @report_params;
+
+  my $report = SL::ReportGenerator->new(\%myconfig, $form, 'std_column_visibility' => 1);
+
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+
+  $report->set_export_options('report', @report_params);
+
+  $report->set_sort_indicator($form->{sort}, $form->{sortdir});
+
+  $report->set_options('raw_top_info_text'    => $form->parse_html_template('letter/report_top',    { 'OPTIONS' => \@options }),
+                       'raw_bottom_info_text' => $form->parse_html_template('letter/report_bottom', { 'HIDDEN'  => \@hidden_report_params }),
+                       'output_format'        => 'HTML',
+                       'title'                => $form->{title},
+                       'attachment_basename'  => $locale->text('letters_list') . strftime('_%Y%m%d', localtime time),
+    );
+  $report->set_options_from_form();
+
+  my $idx      = 0;
+  my $callback = build_std_url('action=report', grep { $form->{$_} } @report_params);
+  my $edit_url = build_std_url('action=edit', 'callback=' . E($callback));
+
+  foreach my $l (@letters) {
+    $idx++;
+
+    my $row = { map { $_ => { 'data' => $l->{$_} } } keys %{ $l } };
+
+    $row->{subject}->{link}      = $edit_url . '&id=' . Q($l->{id});
+    $row->{letternumber}->{link} = $edit_url . '&id=' . Q($l->{id});
+
+    $report->add_data($row);
+  }
+
+  $report->generate_with_headers();
+
+  $lxdebug->leave_sub();
+}
+
+sub print_letter {
+  $lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_edit');
+
+  my ($old_form) = @_;
+
+  my $display_form = $form->{display_form} || "display_form";
+  my $letter       = _update();
+
+  $letter->export_to($form);
+  $form->{formname} = "letter";
+  $form->{format} = "pdf";
+
+  my $language_saved      = $form->{language_id};
+  my $greeting_saved      = $form->{greeting};
+  my $cp_id_saved         = $form->{cp_id};
+
+  call_sub("customer_details");
+
+  if (!$cp_id_saved) {
+    # No contact was selected. Delete all contact variables because
+    # IS->customer_details() and IR->vendor_details() get the default
+    # contact anyway.
+    map({ delete($form->{$_}); } grep(/^cp_/, keys(%{ $form })));
+  }
+
+  $form->{greeting} = $greeting_saved;
+  $form->{language_id} = $language_saved;
+
+  if ($form->{cp_id}) {
+    CT->get_contact(\%myconfig, $form);
+  }
+
+  $form->{cp_contact_formal} = ($form->{cp_greeting} ? "$form->{cp_greeting} " : '') . ($form->{cp_givenname} ? "$form->{cp_givenname} " : '') . $form->{cp_name};
+
+  $form->get_employee_data('prefix' => 'employee', 'id' => $letter->{employee_id});
+  $form->get_employee_data('prefix' => 'salesman', 'id' => $letter->{salesman_id});
+
+  my %create_params = (
+    template  => scalar(SL::Helper::CreatePDF->find_template(
+      name        => 'letter',
+      printer_id  => $::form->{printer_id},
+      language_id => $::form->{language_id},
+      formname    => 'letter',
+      format      => 'pdf',
+    )),
+    variables => $::form,
+    return    => 'file_name',
+  );
+  my $pdf_file_name;
+  eval {
+    $pdf_file_name = SL::Helper::CreatePDF->create_pdf(%create_params);
+
+    if ( $::form->{media} eq 'email') {
+      my $mail             = Mailer->new;
+      my $signature        = $::myconfig{signature};
+      $mail->{$_}          = $::form->{$_}               for qw(cc subject message bcc to);
+      $mail->{from}        = qq|"$::myconfig{name}" <$::myconfig{email}>|;
+      $mail->{fileid}      = time() . '.' . $$ . '.';
+      $mail->{attachments} =  [{ "filename" => $pdf_file_name,
+                                 "name"     => $::form->{attachment_name} }];
+      $mail->{message}    .=  "\n-- \n$signature";
+      $mail->{message}     =~ s/\r//g;
+
+      my $err = $mail->send;
+# TODO
+#       $self
+#           ->js
+#           ->flash($err?'error':'info',
+#                   $err?t8('A mail error occurred: #1', $err):
+#                        t8('The document have been sent to \'#1\'.', $mail->{to}))
+#           ->render($self);
+      return $err?0:1;
+    }
+
+    if (!$::form->{printer_id}) {
+      my $file = IO::File->new($pdf_file_name, 'r') || croak("Cannot open file '$pdf_file_name'");
+      my $size = -s $pdf_file_name;
+      my $content_type    =  'application/pdf';
+      my $attachment_name =  $::form->generate_attachment_filename;
+      $attachment_name    =~ s:.*//::g;
+
+      print $::form->create_http_response(content_type        => $content_type,
+                                          content_disposition => 'attachment; filename="' . $attachment_name . '"',
+                                          content_length      => $size);
+
+      $::locale->with_raw_io(\*STDOUT, sub { print while <$file> });
+      $file->close;
+      unlink $pdf_file_name;
+      return 1;
+    }
+
+    my $printer = SL::DB::Printer->new(id => $::form->{printer_id})->load;
+    my $command = SL::Template::create(type => 'ShellCommand', form => Form->new(''))->parse($printer->printer_command);
+
+    open my $out, '|-', $command or die $!;
+    binmode $out;
+    print $out scalar(read_file($pdf_file_name));
+    close $out;
+
+    flash_later('info', t8('The documents have been sent to the printer \'#1\'.', $printer->printer_description));
+    my $callback = build_std_url('letter.pl', 'action=edit', 'id=' . $letter->{id}, 'printer_id');
+    $::form->redirect;
+    1;
+  } or do {
+    unlink $pdf_file_name;
+    $::form->error(t8("Creating the PDF failed:") . " " . $@);
+  };
+
+  $lxdebug->leave_sub();
+}
+
+sub update {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+
+  my $name_selected = shift;
+
+  _display(
+    letter => _update(
+      _name_selected => $name_selected,
+    ),
+  );
+
+  $::lxdebug->leave_sub;
+}
+
+sub _update {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+
+  my %params = @_;
+
+  my $from_letter = $::form->{letter};
+
+  my $letter      = SL::Letter->new( id => $from_letter->{id} )
+                              ->load
+                              ->update_from($from_letter);
+
+  $letter->check_name(%params);
+  $letter->check_date;
+  $letter->set_greetings;
+
+  $::lxdebug->leave_sub;
+
+  return $letter;
+}
+
+sub letter_tab {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+
+  my @report_params = qw(letternumber subject contact date);
+
+  my @letters       = SL::Letter->find(map { $_ => $form->{$_} } @report_params);
+
+  $::lxdebug->leave_sub;
+}
+
+sub e_mail {
+  $::lxdebug->enter_sub;
+
+  $main::auth->assert('sales_letter_edit');
+  my $letter = _update();
+
+  $letter->check_number;
+  $letter->save;
+
+  $letter->export_to($::form);
+
+  $::form->{id} = $letter->{id};
+  edit_e_mail();
+
+  $::lxdebug->leave_sub;
+}
+
+sub dispatcher {
+  $main::lxdebug->enter_sub();
+  # dispatch drafts
+  my $locale   = $main::locale;
+
+
+  if ($form->{letter_draft_action} eq $locale->text("Skip")) {
+    $form->{DONT_LOAD_DRAFT} = 1;
+    add();
+    return 1;
+  } elsif ($form->{letter_draft_action} eq $locale->text("Delete drafts")) {
+    delete_letter_drafts();
+    return 1;
+  }
+
+  foreach my $action (qw(e_mail print save update save_letter_draft)) {
+    if ($::form->{"action_${action}"}) {
+      $::form->{dispatched_action} = $action;
+      call_sub($action);
+      return;
+    }
+  }
+
+  $::form->error($::locale->text('No action defined.'));
+  $::lxdebug->leave_sub;
+}
+
+sub continue {
+  call_sub($form->{nextsub});
+}
+
+
+sub load_letter_draft {
+  $lxdebug->enter_sub();
+
+  $main::auth->assert('sales_letter_edit');
+ $main::lxdebug->leave_sub() and return 0 if ($form->{DONT_LOAD_DRAFT});
+ $form->{title}    = $locale->text('Letter Draft');
+ $form->{script}   = 'letter.pl';
+
+  my @letter_drafts = SL::Letter->find(draft => 1);
+
+  return unless @letter_drafts;
+  $form->header();
+  print $form->parse_html_template('letter/load_drafts', { LETTER_DRAFTS => \@letter_drafts });
+
+  return 1;
+  $lxdebug->leave_sub();
+}
+
+1;
index a10a672..c08976b 100755 (executable)
@@ -53,6 +53,7 @@ $self->{texts} = {
   'A directory with the name for the new print templates exists already.' => 'Ein Verzeichnis mit dem selben Namen wie die neuen Druckvorlagen existiert bereits.',
   'A lot of the usability of kivitendo has been enhanced with javascript. Although it is currently possible to use every aspect of kivitendo without javascript, we strongly recommend it. In a future version this may change and javascript may be necessary to access advanced features.' => 'Die Bedienung von kivitendo wurde an vielen Stellen mit Javascript verbessert. Obwohl es derzeit möglich ist, jeden Aspekt von kivitendo auch ohne Javascript zu benutzen, empfehlen wir es. In einer zukünftigen Version wird Javascript eventuell notwendig sein um weitergehende Features zu benutzen.',
   'A lower-case character is required.' => 'Ein Kleinbuchstabe ist vorgeschrieben.',
+  'A mail error occurred: #1'   => 'Ein ',
   'A special character is required (valid characters: #1).' => 'Ein Sonderzeichen ist vorgeschrieben (gültige Zeichen: #1).',
   'A transaction description is required.' => 'Die Vorgangsbezeichnung muss eingegeben werden.',
   'A unit with this name does already exist.' => 'Eine Einheit mit diesem Namen existiert bereits.',
@@ -146,6 +147,7 @@ $self->{texts} = {
   'Add Group'                   => 'Warengruppe erfassen',
   'Add Language'                => 'Sprache hinzufügen',
   'Add Lead'                    => 'Kundenquelle erfassen',
+  'Add Letter'                  => 'Brief hinzufügen',
   'Add Machine'                 => 'Maschine erfassen',
   'Add Part'                    => 'Ware erfassen',
   'Add Price Factor'            => 'Preisfaktor erfassen',
@@ -170,6 +172,7 @@ $self->{texts} = {
   'Add User Group'              => 'Neue Benutzergruppe',
   'Add Vendor'                  => 'Lieferant erfassen',
   'Add Vendor Invoice'          => 'Einkaufsrechnung erfassen',
+  'Add Vendor Letter'           => '',
   'Add Warehouse'               => 'Lager erfassen',
   'Add and edit units'          => 'Einheiten erfassen und bearbeiten',
   'Add bank account'            => 'Bankkonto erfassen',
@@ -223,6 +226,7 @@ $self->{texts} = {
   'Allow direct creation of new purchase delivery orders' => 'Direktes Anlegen neuer Einkaufslieferscheine zulassen',
   'Allow direct creation of new purchase invoices' => 'Direktes Anlegen neuer Einkaufsrechnungen zulassen',
   'Allow the following users access to my follow-ups:' => 'Erlaube den folgenden Benutzern Zugriff auf meine Wiedervorlagen:',
+  'Already as letter saved.'    => 'Wurde schon als Brief gespeichert.',
   'Alternatively you can create a new part which will then be selected.' => 'Sie k&ouml;nnen auch einen neuen Artikel anlegen, der dann automatisch ausgew&auml;hlt wird.',
   'Amended Advance Turnover Tax Return' => 'Berichtigte Anmeldung',
   'Amended Advance Turnover Tax Return (Nr. 10)' => 'Ist dies eine berichtigte Anmeldung? (Nr. 10/Zeile 15 Steuererklärung)',
@@ -659,6 +663,7 @@ $self->{texts} = {
   'Created by'                  => 'Erstellt von',
   'Created for'                 => 'Erstellt f&uuml;r',
   'Created on'                  => 'Erstellt am',
+  'Creating the PDF failed:'    => 'PDF-Erzeugung fehlgeschlagen:',
   'Creation Date'               => 'Erstelldatum',
   'Credit'                      => 'Haben',
   'Credit (one letter abbreviation)' => 'H',
@@ -757,6 +762,7 @@ $self->{texts} = {
   'Datevautomatik'              => 'Datev-Automatik',
   'Datum von'                   => 'Datum von',
   'Deactivate by default'       => 'Deaktiviert als Voreinstellung',
+  'Dear Sir or Madam,'          => 'Sehr geehrte Damen und Herren,',
   'Debit'                       => 'Soll',
   'Debit (one letter abbreviation)' => 'S',
   'Debit Account'               => 'Sollkonto',
@@ -771,7 +777,7 @@ $self->{texts} = {
   'Default (no language selected)' => 'Standard (keine Sprache ausgewählt)',
   'Default Accounts'            => 'Standardkonten',
   'Default Bin'                 => 'Standard-Lagerplatz',
-  'Default Bin with ignoring onhand' => 'Standard-Lagerplatz für Lagerbewegungen ohne Überprüfung auf verfügbare Menge ',
+  'Default Bin with ignoring onhand' => 'Standard-Lagerplatz für Auslagern ohne Prüfung auf Bestand',
   'Default Client (unconfigured)' => 'Standardmandant (unkonfiguriert)',
   'Default Customer/Vendor Language' => 'Standard-Kunden-/Lieferantensprache',
   'Default Transfer'            => 'Ein- / Auslagern über Standardlagerplätze',
@@ -780,7 +786,7 @@ $self->{texts} = {
   'Default Transfer with Master Bin' => 'Standardlagerplatz für Lagerbewegungen verwenden, falls keiner in Stammdaten definiert',
   'Default Transfer with services' => 'Ein- /Auslagern von Dienstleistungen über Standard-Lagerplatz',
   'Default Warehouse'           => 'Standard-Lager',
-  'Default Warehouse with ignoring on hand' => 'Standardlager für Auslagern ohne Prüfung auf Bestand',
+  'Default Warehouse with ignoring onhand' => 'Standard-Lager für Auslagern ohne Prüfung auf Bestand',
   'Default article for converting into quotations and orders' => 'Standardartikel für Konvertierung von Pflichtenheften in Angebote und Aufträge',
   'Default buchungsgruppe'      => 'Standardbuchungsgruppe',
   'Default client'              => 'Standardmandant',
@@ -791,6 +797,8 @@ $self->{texts} = {
   'Default printer'             => 'Standarddrucker',
   'Default taxzone'             => 'Standardsteuerzone',
   'Default template format'     => 'Standardvorlagenformat',
+  'Default transfer delivery order' => 'Standard-Auslagern über Lieferschein',
+  'Default transfer invoice'    => 'Standard-Auslagern über Rechnung',
   'Default transport article number' => 'Standard Versand / Transport-Erinnerungs-Artikel',
   'Default unit'                => 'Standardeinheit',
   'Default value'               => 'Standardwert',
@@ -907,6 +915,7 @@ $self->{texts} = {
   'Download SEPA XML export file' => 'SEPA-XML-Exportdatei herunterladen',
   'Download picture'            => 'Bild herunterladen',
   'Download sample file'        => 'Beispieldatei herunterladen',
+  'Draft for this Letter saved!' => 'Briefentwurf gespeichert!',
   'Draft saved.'                => 'Entwurf gespeichert.',
   'Drawing'                     => 'Zeichnung',
   'Dropdown Limit'              => 'Auswahllistenbegrenzung',
@@ -965,6 +974,7 @@ $self->{texts} = {
   'Edit Group'                  => 'Warengruppe editieren',
   'Edit Language'               => 'Sprache bearbeiten',
   'Edit Lead'                   => 'Kundenquelle bearbeiten',
+  'Edit Letter'                 => 'Brief bearbeiten',
   'Edit Part'                   => 'Ware bearbeiten',
   'Edit Preferences for #1'     => 'Einstellungen von #1 bearbeiten',
   'Edit Price Factor'           => 'Preisfaktor bearbeiten',
@@ -1454,6 +1464,11 @@ $self->{texts} = {
   'Lead'                        => 'Kundenquelle',
   'Leads'                       => 'Kundenquellen',
   'Left'                        => 'Links',
+  'Letter'                      => 'Brief',
+  'Letter Draft'                => 'Briefentwurf',
+  'Letter saved!'               => 'Brief gespeichert!',
+  'Letternumber'                => 'Briefnummer',
+  'Letters'                     => 'Briefe',
   'Liability'                   => 'Passiva/Mittelherkunft',
   'Limit part selection'        => 'Artikelauswahl eingrenzen',
   'Line Total'                  => 'Zeilensumme',
@@ -1480,6 +1495,7 @@ $self->{texts} = {
   'List of tax zones'           => 'Liste der Steuerzonen',
   'List open SEPA exports'      => 'Noch nicht ausgeführte SEPA-Exporte anzeigen',
   'Load draft'                  => 'Entwurf laden',
+  'Load letter draft'           => 'Briefentwurf laden',
   'Load profile'                => 'Profil laden',
   'Loading...'                  => 'Wird geladen...',
   'Local Tax Office Preferences' => 'Angaben zum Finanzamt',
@@ -1674,6 +1690,7 @@ $self->{texts} = {
   'Not delivered'               => 'Nicht geliefert',
   'Not done yet'                => 'Noch nicht fertig',
   'Not obsolete'                => 'Gültig',
+  'Not yet implemented!'        => 'Noch nicht implementiert!',
   'Note'                        => 'Hinweis',
   'Note: Taxkeys must have a "valid from" date, and will not behave correctly without.' => 'Hinweis: Steuerschlüssel sind fehlerhaft ohne "Gültig ab" Datum',
   'Notes'                       => 'Bemerkungen',
@@ -1902,6 +1919,7 @@ $self->{texts} = {
   'Prepare bank collection via SEPA XML' => 'Einzug via SEPA XML vorbereiten',
   'Prepare bank transfer via SEPA XML' => 'Überweisung via SEPA XML vorbereiten',
   'Prepayment'                  => 'Vorauszahlung',
+  'Presskit'                    => 'Pressemappe',
   'Preview'                     => 'Druckvorschau',
   'Preview Mode'                => 'Vorschaumodus',
   'Previous transdate text'     => 'wurde gespeichert am',
@@ -2322,6 +2340,7 @@ $self->{texts} = {
   'Since bin is not enforced in the parts data, please specify a bin where goods without a specified bin will be put.' => 'Da Lagerpl&auml;tze kein Pflichtfeld sind, geben Sie bitte einen Lagerplatz an, in dem Waren ohne spezifizierten Lagerplatz eingelagert werden sollen.',
   'Single quotes'               => 'Einfache Anführungszeichen',
   'Single values in item mode, cumulated values in invoice mode' => 'Einzelwerte im Artikelmodus, kumulierte Werte im Rechnungsmodus',
+  'Sketch'                      => 'Skizze',
   'Skip'                        => 'Überspringen',
   'Skip entry'                  => 'Eintrag überspringen',
   'Skipping due to existing entry in database' => 'Wegen existierendem Eintrag mit selber Nummer übersprungen',
@@ -2500,6 +2519,7 @@ $self->{texts} = {
   'The base unit does not exist.' => 'Die Basiseinheit existiert nicht.',
   'The base unit relations must not contain loops (e.g. by saying that unit A\'s base unit is B, B\'s base unit is C and C\'s base unit is A) in row %d.' => 'Die Beziehungen der Einheiten d&uuml;rfen keine Schleifen beinhalten (z.B. wenn gesagt wird, dass Einheit As Basiseinheit B, Bs Basiseinheit C und Cs Basiseinheit A ist) in Zeile %d.',
   'The basic client tables have not been created for this client\'s database yet.' => 'Die grundlegenden Mandantentabellen wurden in der für diesen Mandanten konfigurierten Datenbank noch nicht angelegt.',
+  'The body is missing.'        => 'Der Text fehlt',
   'The buchungsgruppe has been deleted.' => 'Die Buchungsgruppe wurde gelöscht.',
   'The buchungsgruppe is in use and cannot be deleted.' => 'Die Buchungsgruppe wird benutzt und kann daher nicht gelöscht werden.',
   'The business has been created.' => 'Der Kunden-/Lieferantentyp wurde erfasst.',
@@ -2542,6 +2562,7 @@ $self->{texts} = {
   'The database user is missing.' => 'Der Datenbankbenutzer fehlt.',
   'The dataset #1 has been created.' => 'Die Datenbank #1 wurde angelegt.',
   'The dataset #1 has been deleted.' => 'Die Datenbank #1 wurde gelöscht.',
+  'The date is missing.'        => 'Das Datum fehlt',
   'The deductible amount'       => 'Der abziehbare Skontobetrag',
   'The default delivery plan only checks if all delivery orders have been created not if the goods are transferred. This feature will check if all the goods are transferred. Caveat: Only the state of the delivery orders are checked not partial transferred delivery orders (in technical terms: the table inventory is not checked' => 'Standardmässig wird beim Lieferplan überprüft, ob es eine vollständige Liefermenge über alle Lieferscheine gibt. Dies ist dann die Statusbedingung für geliefert oder nicht geliefert. Mit dieser Erweiterung wird geprüft ob die Lieferbelege auch wirklich ausgelagert sind oder nicht. Teilausgelagerte Lieferscheine werden allerdings nicht berücksichtigt (Technischer Hintergrund: Keine Überprüfung der Lagertabelle inventory).',
   'The default value depends on the variable type:' => 'Die Bedeutung des Standardwertes h&auml;ngt vom Variablentypen ab:',
@@ -2562,8 +2583,11 @@ $self->{texts} = {
   'The discount must be less than 100%.' => 'Der Rabatt muss kleiner als 100% sein.',
   'The discount must not be negative.' => 'Der Rabatt darf nicht negativ sein.',
   'The discounted amount will be shown in documents.' => 'Der Rabattbetrag wird in Belegen ausgewiesen.',
+  'The document have been sent to \'#1\'.' => 'Das Dokument wurde an \'#1\' geschickt.',
+  'The documents have been sent to the printer \'#1\'.' => 'Die Dokumente wurden an den Drucker \'#1\' geschickt.',
   'The dunning process started' => 'Der Mahnprozess ist gestartet.',
   'The dunnings have been printed.' => 'Die Mahnung(en) wurden gedruckt.',
+  'The employee is missing.'    => 'Der Bearbeiter fehlt.',
   'The end date is the last day for which invoices will possibly be created.' => 'Das Enddatum ist das letztmögliche Datum, an dem eine Rechnung erzeugt wird.',
   'The execution schedule is invalid.' => 'Der Ausführungszeitplan ist ungültig.',
   'The execution type is invalid.' => 'Der Ausführungstyp ist ungültig.',
@@ -2576,6 +2600,7 @@ $self->{texts} = {
   'The following currencies have been used, but they are not defined:' => 'Die folgenden Währungen wurden benutzt, sind aber nicht ordnungsgemäß in der Datenbank eingetragen:',
   'The following drafts have been saved and can be loaded.' => 'Die folgenden Entw&uuml;rfe wurden gespeichert und k&ouml;nnen geladen werden.',
   'The following groups are valid for this client' => 'Die folgenden Gruppen sind für diesen Mandanten gültig',
+  'The following letter drafts have been saved and can be loaded.' => 'Die folgenden Briefentwürfe sind vorhanden und können geladen werden.',
   'The following list has been generated automatically from existing users collapsing users with identical settings into a single entry.' => 'Die folgende Liste wurde automatisch aus den im System vorhandenen Benutzern zusammengestellt, wobei identische Einstellungen zu einem Eintrag zusammengefasst wurden.',
   'The following old files whose settings have to be merged manually into the new configuration file "config/kivitendo.conf" still exist:' => 'Es existieren noch die folgenden alten Dateien, deren Einstellungen manuell in die neue Konfiguratsdatei "config/kivitendo.conf" migriert werden müssen:',
   'The following transaction contains wrong taxes:' => 'Die folgende Buchung enthält falsche Steuern:',
@@ -2776,8 +2801,8 @@ $self->{texts} = {
   'This has been changed in this version.' => 'Ab dieser Version ist dies nicht mehr so.',
   'This is a very critical problem.' => 'Dieses Problem ist sehr schwerwiegend.',
   'This is the client to be selected by default on the login screen.' => 'Dies ist derjenige Mandant, der im Loginbildschirm standardmäßig ausgewählt sein wird.',
-  'This is the default bin for ignoring onhand' => 'Standardlagerplatz für Auslagern ohne Bestandsprüfung',
   'This is the default bin for parts' => 'Standard-Lagerplatz für Stammdaten/Waren',
+  'This is the default warehouse for ignoring onhand' => 'Standardlager für Auslagern ohne Prüfung auf Bestand.',
   'This list is capped at 15 items to keep it fast. If you need a full list, please use reports.' => 'Diese Liste ist auf 15 Zeilen begrenzt. Wenn Sie eine vollständige Liste benötigen, erstellen Sie bitte einen Bericht.',
   'This means that the user has created an AP transaction and chosen a taxkey for sales taxes, or that he has created an AR transaction and chosen a taxkey for input taxes.' => 'Das bedeutet, dass ein Benutzer eine Kreditorenbuchung angelegt und in ihr einen Umsatzsteuer-Steuerschlüssel verwendet oder eine Debitorenbuchung mit Vorsteuer-Steuerschlüssel angelegt hat.',
   'This module can help you identify and correct such entries by analyzing the general ledger and presenting you likely solutions but also allowing you to fix problems yourself.' => 'Dieses Modul kann Ihnen helfen, problematische Einträge im Hauptbuch zu identifizieren und teilweise zu beheben. Dabei werden je nach Problem mögliche Lösungen aufgezeigt, wobei Sie die entscheiden können, welche Probleme automatisch gelöst werden sollen.',
@@ -2962,6 +2987,7 @@ $self->{texts} = {
   'Vendor Discount'             => 'Lieferantenrabatt',
   'Vendor Invoice'              => 'Einkaufsrechnung',
   'Vendor Invoices & AP Transactions' => 'Einkaufsrechnungen & Kreditorenbuchungen',
+  'Vendor Letters'              => '',
   'Vendor Name'                 => 'Lieferantenname',
   'Vendor Number'               => 'Lieferantennummer',
   'Vendor Order Number'         => 'Bestellnummer beim Lieferanten',
@@ -3064,6 +3090,7 @@ $self->{texts} = {
   'You have to grant users access to one or more clients.' => 'Benutzern muss dann Zugriff auf einzelne Mandanten gewährt werden.',
   'You have to specify a department.' => 'Sie müssen eine Abteilung wählen.',
   'You have to specify an execution date for each antry.' => 'Sie müssen für jeden zu buchenden Eintrag ein Ausführungsdatum angeben.',
+  'You must choose a user.'     => '',
   'You must chose a user.'      => 'Sie m&uuml;ssen einen Benutzer ausw&auml;hlen.',
   'You must enter a name for your new print templates.' => 'Sie müssen einen Namen für die neuen Druckvorlagen angeben.',
   'You must select existing print templates or create a new set.' => 'Sie müssen vorhandene Druckvorlagen auswählen oder einen neuen Satz anlegen.',
@@ -3071,6 +3098,7 @@ $self->{texts} = {
   'You\'re not editing a file.' => 'Sie bearbeiten momentan keine Datei.',
   'You\'ve already chosen the following limitations:' => 'Sie haben bereits die folgenden Einschr&auml;nkungen vorgenommen:',
   'Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.' => 'Ihre PostgreSQL-Installation benutzt ein anderes Encoding als Unicode. Dies wird nicht mehr unterstützt.',
+  'Your Reference'              => 'Ihr Zeichen',
   'Your TODO list'              => 'Ihre Aufgabenliste',
   'Your account number'         => 'Ihre Kontonummer',
   'Your bank'                   => 'Der Name Ihrer Bank',
@@ -3110,6 +3138,7 @@ $self->{texts} = {
   'bis'                         => 'bis',
   'building data'               => 'Verarbeite Daten',
   'building report'             => 'Erstelle Bericht',
+  'button'                      => '',
   'cash'                        => 'Ist-Versteuerung',
   'chargenumber #1'             => 'Chargennummer #1',
   'chart_of_accounts'           => 'kontenuebersicht',
@@ -3186,6 +3215,7 @@ $self->{texts} = {
   'kivitendo will then update the database automatically.' => 'kivitendo wird die Datenbank daraufhin automatisch aktualisieren.',
   'lead deleted!'               => 'Kundenquelle gelöscht',
   'lead saved!'                 => 'Kundenquelle geichert',
+  'letters_list'                => '',
   'list'                        => 'auflisten',
   'list_of_payments'            => 'zahlungsausgaenge',
   'list_of_receipts'            => 'zahlungseingaenge',
index c9397c3..c968475 100644 (file)
@@ -147,6 +147,10 @@ type=credit_note
 ACCESS=dunning_edit
 module=dn.pl
 action=add
+[AR--Add Letter]
+ACCESS=sales_letter_edit
+module=letter.pl
+action=add
 
 [AR--Reports]
 module=menu.pl
@@ -213,6 +217,11 @@ ACCESS=sales_order_edit
 module=controller.pl
 action=FinancialControllingReport/list
 
+[AR--Reports--Letters]
+ACCESS=sales_letter_report
+module=letter.pl
+action=search
+
 [AP]
 
 [AP--Add RFQ]
diff --git a/sql/Pg-upgrade2-auth/sales_letter_rights.pl b/sql/Pg-upgrade2-auth/sales_letter_rights.pl
new file mode 100644 (file)
index 0000000..22c9787
--- /dev/null
@@ -0,0 +1,27 @@
+# @tag: sales_letter_rights
+# @description: Setzt das neue Recht den Lieferplan anzuzeigen
+# @depends: release_3_2_0
+package SL::DBUpgrade2::sales_letter_rights;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use SL::DBUtils;
+
+sub run {
+  my ($self) = @_;
+
+  my $groups = $main::auth->read_groups();
+
+  foreach my $group (values %{$groups}) {
+    $group->{rights}->{sales_letter_edit} = $group->{rights}->{sales_order_edit};
+    $group->{rights}->{sales_letter_report} = $group->{rights}->{sales_order_edit};
+    $main::auth->save_group($group);
+  }
+
+  return 1;
+} # end run
+
+1;
diff --git a/sql/Pg-upgrade2/letter.sql b/sql/Pg-upgrade2/letter.sql
new file mode 100644 (file)
index 0000000..6aedbd6
--- /dev/null
@@ -0,0 +1,41 @@
+-- @tag: letter
+-- @description: Brieffunktion Felder
+-- @depends: release_3_2_0
+
+CREATE TABLE letter (
+  id INTEGER NOT NULL DEFAULT nextval('id'),
+  vc_id INTEGER NOT NULL,
+  rcv_name TEXT,
+  rcv_contact TEXT,
+  rcv_address TEXT,
+  rcv_countrycode TEXT,
+  rcv_zipcode TEXT,
+  rcv_city TEXT,
+
+  letternumber TEXT,
+  jobnumber TEXT,
+  text_created_for TEXT,
+  date TEXT,
+
+  subject TEXT,
+  greeting TEXT,
+  body TEXT,
+  close TEXT,
+  company_name TEXT,
+
+  employee_id INTEGER NOT NULL,
+  employee_position TEXT,
+
+  salesman_id INTEGER NOT NULL,
+  salesman_position TEXT,
+
+  itime TIMESTAMP DEFAULT now(),
+  mtime TIMESTAMP,
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (employee_id) REFERENCES employee (id),
+  FOREIGN KEY (salesman_id) REFERENCES employee (id)
+);
+
+ALTER TABLE defaults ADD COLUMN letternumber integer;
+
diff --git a/sql/Pg-upgrade2/letter_country_page.sql b/sql/Pg-upgrade2/letter_country_page.sql
new file mode 100644 (file)
index 0000000..59e5f85
--- /dev/null
@@ -0,0 +1,7 @@
+-- @tag: letter_country_page
+-- @description: Brieffunktion Felder Update
+-- @depends: letter
+
+ALTER TABLE letter ADD COLUMN rcv_country TEXT;
+ALTER TABLE letter ADD COLUMN page_created_for TEXT;
+
diff --git a/sql/Pg-upgrade2/letter_cp_id.sql b/sql/Pg-upgrade2/letter_cp_id.sql
new file mode 100644 (file)
index 0000000..565f4de
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: letter_cp_id
+-- @description: Ansprechpartner Link
+-- @depends: letter_notes_internal
+
+ALTER TABLE letter ADD COLUMN cp_id integer;
+ALTER TABLE letter ADD FOREIGN KEY (cp_id) REFERENCES contacts(cp_id);
diff --git a/sql/Pg-upgrade2/letter_date_type.sql b/sql/Pg-upgrade2/letter_date_type.sql
new file mode 100644 (file)
index 0000000..8c6293a
--- /dev/null
@@ -0,0 +1,9 @@
+-- @tag: letter_date_type
+-- @description: Briefe: Datumsfeld als Datum speichern
+-- @depends: release_3_2_0 letter
+-- @encoding: utf-8
+ALTER TABLE letter ADD column date_date DATE;
+UPDATE letter SET date_date = date::DATE;
+ALTER TABLE letter DROP COLUMN date;
+ALTER TABLE letter RENAME COLUMN date_date TO date;
+
diff --git a/sql/Pg-upgrade2/letter_draft.sql b/sql/Pg-upgrade2/letter_draft.sql
new file mode 100644 (file)
index 0000000..2cf2741
--- /dev/null
@@ -0,0 +1,44 @@
+-- @tag: letter_draft
+-- @description: Briefentwürfe Felder
+-- @depends: release_3_2_0 letter
+
+CREATE TABLE letter_draft (
+  id INTEGER NOT NULL DEFAULT nextval('id'),
+  vc_id INTEGER NOT NULL,
+  cp_id INTEGER,
+  rcv_name TEXT,
+  rcv_contact TEXT,
+  rcv_address TEXT,
+  rcv_countrycode TEXT,
+  rcv_zipcode TEXT,
+  rcv_city TEXT,
+  rcv_country TEXT,
+  page_created_for TEXT,
+  letternumber TEXT,
+  jobnumber TEXT,
+  text_created_for TEXT,
+  date DATE,
+  intnotes TEXT,
+
+  reference TEXT,
+  subject TEXT,
+  greeting TEXT,
+  body TEXT,
+  close TEXT,
+  company_name TEXT,
+
+  employee_id INTEGER,
+  employee_position TEXT,
+
+  salesman_id INTEGER,
+  salesman_position TEXT,
+
+  itime TIMESTAMP DEFAULT now(),
+  mtime TIMESTAMP,
+
+  PRIMARY KEY (id),
+  FOREIGN KEY (employee_id) REFERENCES employee (id),
+  FOREIGN KEY (salesman_id) REFERENCES employee (id),
+  FOREIGN KEY (cp_id) REFERENCES contacts(cp_id)
+);
+
diff --git a/sql/Pg-upgrade2/letter_employee_salesman.sql b/sql/Pg-upgrade2/letter_employee_salesman.sql
new file mode 100644 (file)
index 0000000..87b89ed
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: letter_emplyee_salesman
+-- @description: Briefe: Fußfelder sind nicht mehr Pflicht.
+-- @depends: letter_country_page
+
+ALTER TABLE letter ALTER COLUMN employee_id DROP NOT NULL;
+ALTER TABLE letter ALTER COLUMN salesman_id DROP NOT NULL;
+
+
diff --git a/sql/Pg-upgrade2/letter_notes_internal.sql b/sql/Pg-upgrade2/letter_notes_internal.sql
new file mode 100644 (file)
index 0000000..a63f86b
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: letter_notes_internal
+-- @description: Briefe: interne Bemerkungen.
+-- @depends: letter_emplyee_salesman
+
+ALTER TABLE letter ADD COLUMN intnotes text;
diff --git a/sql/Pg-upgrade2/letter_reference.sql b/sql/Pg-upgrade2/letter_reference.sql
new file mode 100644 (file)
index 0000000..521ae6c
--- /dev/null
@@ -0,0 +1,5 @@
+-- @tag: letter_reference
+-- @description: Briefe: Ihr Zeichen
+-- @depends: letter
+
+ALTER TABLE letter ADD COLUMN reference text;
diff --git a/templates/print/RB/letter.tex b/templates/print/RB/letter.tex
new file mode 100644 (file)
index 0000000..1645464
--- /dev/null
@@ -0,0 +1,109 @@
+\documentclass[twoside]{scrartcl}
+\usepackage{eurosym}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\setlength{\voffset}{0.5cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.5cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{19.2cm}
+\setlength{\textheight}{24.5cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\begin{document}
+
+\thispagestyle{empty}
+
+\newlength{\descrwidth}\setlength{\descrwidth}{10cm}
+\setlength{\parindent}{0cm}
+
+\fontfamily{cmss}\fontshape{n}\selectfont
+
+<%pagebreak 80 28 37%>
+\end{tabularx}
+
+\newpage
+
+%\begin{tabularx}{\textwidth}{lrXrr}
+%  \hline
+%  \textbf{Pos} & \textbf{Menge} & \textbf{Bezeichnung} &
+%  \textbf{E-Preis/\euro} & \textbf{G-Preis/\euro} \\
+%  \hline
+%  <%foreach number%>
+%    <%runningnumber%> & <%qty%> <%unit%> & \raggedright <%description%> &
+%    <%sellprice%> & <%linetotal%>\\
+%  <%end number%>
+%<%end pagebreak%>
+
+\fontfamily{cmss}\fontsize{10pt}{12pt}\selectfont
+
+\vspace*{1cm}
+
+\begin{minipage}{14cm}
+
+  <%rcv_name%>
+
+  <%rcv_contact_formal%>
+
+  <%rcv_countrycode%> <%rcv_zipcode%> <%rcv_city%>
+
+  <%rcv_country%>
+
+\end{minipage}
+\hfill
+\begin{minipage}{6cm}
+\end{minipage}
+
+\vspace{2.5cm}
+
+\begin{minipage}{14cm}
+
+\textbf{<%subject%>}
+
+\vspace{1cm}
+
+  <%greeting%>
+
+\vspace{0.5cm}
+
+  <%body%>
+
+\vspace{0.5cm}
+
+  <%close%>
+
+  <%company_name%>
+
+\end{minipage}
+\hfill
+\begin{minipage}{6cm}
+  <%date%>
+
+\vspace{5cm}
+
+Anlagen | Attch.:
+\end{minipage}
+
+\vspace*{0.5cm}
+
+\begin{minipage}{6cm}
+
+\textbf{<%employee%>}
+
+<%employee_position%>
+
+\end{minipage}
+\begin{minipage}{6cm}
+
+\textbf{<%salesman%>}
+
+<%salesman_position%>
+
+\end{minipage}
+
+\end{document}
+
diff --git a/templates/print/Standard/letter.tex b/templates/print/Standard/letter.tex
new file mode 100644 (file)
index 0000000..1645464
--- /dev/null
@@ -0,0 +1,109 @@
+\documentclass[twoside]{scrartcl}
+\usepackage{eurosym}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\setlength{\voffset}{0.5cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.5cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{19.2cm}
+\setlength{\textheight}{24.5cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\begin{document}
+
+\thispagestyle{empty}
+
+\newlength{\descrwidth}\setlength{\descrwidth}{10cm}
+\setlength{\parindent}{0cm}
+
+\fontfamily{cmss}\fontshape{n}\selectfont
+
+<%pagebreak 80 28 37%>
+\end{tabularx}
+
+\newpage
+
+%\begin{tabularx}{\textwidth}{lrXrr}
+%  \hline
+%  \textbf{Pos} & \textbf{Menge} & \textbf{Bezeichnung} &
+%  \textbf{E-Preis/\euro} & \textbf{G-Preis/\euro} \\
+%  \hline
+%  <%foreach number%>
+%    <%runningnumber%> & <%qty%> <%unit%> & \raggedright <%description%> &
+%    <%sellprice%> & <%linetotal%>\\
+%  <%end number%>
+%<%end pagebreak%>
+
+\fontfamily{cmss}\fontsize{10pt}{12pt}\selectfont
+
+\vspace*{1cm}
+
+\begin{minipage}{14cm}
+
+  <%rcv_name%>
+
+  <%rcv_contact_formal%>
+
+  <%rcv_countrycode%> <%rcv_zipcode%> <%rcv_city%>
+
+  <%rcv_country%>
+
+\end{minipage}
+\hfill
+\begin{minipage}{6cm}
+\end{minipage}
+
+\vspace{2.5cm}
+
+\begin{minipage}{14cm}
+
+\textbf{<%subject%>}
+
+\vspace{1cm}
+
+  <%greeting%>
+
+\vspace{0.5cm}
+
+  <%body%>
+
+\vspace{0.5cm}
+
+  <%close%>
+
+  <%company_name%>
+
+\end{minipage}
+\hfill
+\begin{minipage}{6cm}
+  <%date%>
+
+\vspace{5cm}
+
+Anlagen | Attch.:
+\end{minipage}
+
+\vspace*{0.5cm}
+
+\begin{minipage}{6cm}
+
+\textbf{<%employee%>}
+
+<%employee_position%>
+
+\end{minipage}
+\begin{minipage}{6cm}
+
+\textbf{<%salesman%>}
+
+<%salesman_position%>
+
+\end{minipage}
+
+\end{document}
+