Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision a521b29b

Von G. Richardson vor mehr als 12 Jahren hinzugefügt

  • ID a521b29b7f07b940afb392ccbd22f6373dfa0b7e
  • Vorgänger ff641432
  • Nachfolger 44c040d3

Verkaufsberichtsortierung um Land, Warengruppen, Kundentyp, Verkäufer und Monat erweitert

Hauptsortierung und Untersortierung sind jetzt nicht mehr auf Ware und Kunde
begrenzt, sondern man kann eine Kombinationen erstellen aus:

  • Kunde
  • Ware
  • Land
  • Warengruppe
  • Kundentyp
  • Verkäufer
  • Monat

Es kann jetzt auch nach benutzerdefinierten Variablen gefiltert werden.

Der Verkaufsbericht spaltet sich mit seinen Optionen langsam in zwei
unterschiedliche Bereiche auf, den Artikelmodus, wo die einzelnen Zeilen aus
invoice angezeigt werden, und den Rechnungsmodus, wo nur die Zeilen der
Zwischensummen und Summen angezeigt werden, und die Detailinformationen
aus invoice nur stören.

Default ist Rechnungsmodus, den Artikelmodus kann man per Häkchen auswählen.

Je nachdem auf welcher Ebene man sich befindet machen dann auch Informationen
wie "Durchschnittsverkaufspreis" keinen Sinn mehr.

Bei Zuordnungen wo die Sortierung keinen Wert hat (z.B. Sortierung nach Land,
aber beim Kunden ist kein Land hinterlegt), erscheint als Überschrift "leer",
und alle leeren Werte werden als eine Gruppe zusammengefasst.

Unterschiede anzeigen:

bin/mozilla/vk.pl
46 46

  
47 47
use strict;
48 48

  
49

  
50 49
sub search_invoice {
51 50
  $main::lxdebug->enter_sub();
52 51
  $main::auth->assert('general_ledger | invoice_edit');
......
63 62
  $form->{title}    = $locale->text('Sales Report');
64 63
  $form->{jsscript} = 1;
65 64

  
66
  $form->get_lists("projects"     => { "key" => "ALL_PROJECTS", "all" => 1 },
67
                   "departments"  => "ALL_DEPARTMENTS",
68
                   "customers"    => "ALL_VC");
69

  
65
  $form->get_lists("projects"        => { "key" => "ALL_PROJECTS", "all" => 1 },
66
                   "departments"     => "ALL_DEPARTMENTS",
67
                   "business_types"  => "ALL_BUSINESS_TYPES",
68
                   "salesmen"        => "ALL_SALESMEN",
69
                   'employees'       => 'ALL_EMPLOYEES',
70
                   'partsgroup'      => 'ALL_PARTSGROUPS',
71
                   "customers"       => "ALL_VC");
72
  $form->{CUSTOM_VARIABLES_IC}                  = CVar->get_configs('module' => 'IC');
73
  ($form->{CUSTOM_VARIABLES_FILTER_CODE_IC},
74
   $form->{CUSTOM_VARIABLES_INCLUSION_CODE_IC}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES_IC},
75
                                                                           'include_prefix' => 'l_',
76
                                                                           'include_value'  => 'Y');
77

  
78
  $form->{CUSTOM_VARIABLES_CT}                  = CVar->get_configs('module' => 'CT');
79
  ($form->{CUSTOM_VARIABLES_FILTER_CODE_CT},
80
   $form->{CUSTOM_VARIABLES_INCLUSION_CODE_CT}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES_CT},
81
                                                                           'include_prefix' => 'l_',
82
                                                                           'include_value'  => 'Y');
70 83
  $form->{vc_keys}   = sub { "$_[0]->{name}--$_[0]->{id}" };
84
  $form->{employee_labels} = sub { $_[0]->{"name"} || $_[0]->{"login"} };
85
  $form->{salesman_labels} = $form->{employee_labels};
71 86

  
72 87
  $form->header;
73 88
  print $form->parse_html_template('vk/search_invoice', { %myconfig });
......
86 101

  
87 102
  my ($callback, $href, @columns);
88 103

  
104
  # can't currently be configured from report, empty line between main sortings
105
  my $addemptylines = '1';
106

  
89 107
  if ( $form->{customer} =~ /--/ ) {
90 108
    # Felddaten kommen aus Dropdownbox
91 109
    ($form->{customername}, $form->{customer_id}) = split(/--/, $form->{customer});
......
108 126
  # decimalplaces überprüfen oder auf Default 2 setzen
109 127
  $form->{decimalplaces} = 2 unless $form->{decimalplaces} > 0 && $form->{decimalplaces} < 6;
110 128

  
129
  my $cvar_configs_ct = CVar->get_configs('module' => 'CT');
130
  my $cvar_configs_ic = CVar->get_configs('module' => 'IC');
131

  
111 132
#  report_generator_set_default_sort('transdate', 1);
112 133

  
113 134
  VK->invoice_transactions(\%myconfig, \%$form);
114 135

  
115
  # anhand von radio button die Sortierreihenfolge festlegen
116
  if ($form->{sortby} eq 'artikelsort') {
117
    $form->{'mainsort'} = 'parts_id';
118
    $form->{'subsort'}  = 'name';
119
  } else {
120
    $form->{'mainsort'} = 'name';
121
    $form->{'subsort'}  = 'parts_id';
136
  
137
  if ( $form->{mainsort} eq 'month' or $form->{subsort} eq 'month' ) {
138

  
139
    # Data already comes out of SELECT statement in correct month order, but
140
    # remove whitespaces (month names are padded) and translate them as early
141
    # as possible
142

  
143
    foreach (@{ $form->{AR} }) {
144
      $_->{month} =~ s/\s//g;
145
      $_->{month} = $locale->text($_->{month});
146
    };
122 147
  };
123 148

  
124 149
  $form->{title} = $locale->text('Sales Report');
125 150

  
126 151
  @columns =
127
    qw(description invnumber transdate customernumber partnumber transdate qty unit sellprice sellprice_total discount lastcost lastcost_total marge_total marge_percent);
152
    qw(description invnumber transdate customernumber customername partnumber partsgroup country business transdate qty unit sellprice sellprice_total discount lastcost lastcost_total marge_total marge_percent employee salesman);
153

  
154
  my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs_ic }, @{ $cvar_configs_ct };
155
  my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs_ic }, @{ $cvar_configs_ct };
156
  my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
157

  
158
  push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
159

  
128 160

  
129 161
  # hidden variables für pdf/csv export übergeben
130 162
  # einmal mit l_ um zu bestimmen welche Spalten ausgegeben werden sollen
131 163
  # einmal optionen für die Überschrift (z.B. transdatefrom, partnumber, ...)
132
  my @hidden_variables  = (qw(l_headers l_subtotal l_total l_customernumber transdatefrom transdateto decimalplaces customer customername customer_id department partnumber description project_id customernumber), "$form->{db}number", map { "l_$_" } @columns);
164
  my @hidden_variables  = (qw(l_headers_mainsort l_headers_subsort l_subtotal_mainsort l_subtotal_subsort l_total l_parts l_customername l_customernumber transdatefrom transdateto decimalplaces customer customername customer_id department partnumber partsgroup country business description project_id customernumber salesman employee salesman_id employee_id business_id partsgroup_id mainsort subsort), 
165
      "$form->{db}number", 
166
      map({ "cvar_$_->{name}" } @searchable_custom_variables),
167
      map { "l_$_" } @columns
168
      );
133 169
  my @hidden_nondefault = grep({ $form->{$_} } @hidden_variables);
134 170
  # Variablen werden dann als Hidden Variable mitgegeben, z.B.
135 171
  # <input type="hidden" name="report_generator_hidden_transdateto" value="21.05.2010">
......
140 176
  my %column_defs = (
141 177
    'description'             => { 'text' => $locale->text('Description'), },
142 178
    'partnumber'              => { 'text' => $locale->text('Part Number'), },
179
    'partsgroup'              => { 'text' => $locale->text('Group'), },
180
    'country'                 => { 'text' => $locale->text('Country'), },
181
    'business'                => { 'text' => $locale->text('Customer type'), },
182
    'employee'                => { 'text' => $locale->text('Employee'), },
183
    'salesman'                => { 'text' => $locale->text('Salesperson'), },
143 184
    'invnumber'               => { 'text' => $locale->text('Invoice Number'), },
144 185
    'transdate'               => { 'text' => $locale->text('Invoice Date'), },
145 186
    'qty'                     => { 'text' => $locale->text('Quantity'), },
......
152 193
    'marge_total'             => { 'text' => $locale->text('Sales margin'), },
153 194
    'marge_percent'           => { 'text' => $locale->text('Sales margin %'), },
154 195
    'customernumber'          => { 'text' => $locale->text('Customer Number'), },
196
    'customername'            => { 'text' => $locale->text('Customer Name'), },
197
# add 3 more column_defs so we have a translation for top_info_text
198
    'customer'                => { 'text' => $locale->text('Customer'), },
199
    'part'                    => { 'text' => $locale->text('Part'), },
200
    'month'                   => { 'text' => $locale->text('Month'), },
201
    %column_defs_cvars,
155 202
  );
156 203

  
204
  map { $column_defs{$_}->{visible} = $form->{"l_$_"} eq 'Y' } @columns;
205

  
157 206
  my %column_alignment = map { $_ => 'right' } qw(lastcost sellprice sellprice_total lastcost_total unit discount marge_total marge_percent qty);
158 207

  
159
  $form->{"l_type"} = "Y";
208
  
209
  # so now the check-box "Description" is only used as switch for part description in invoice-mode
210
  # always fill the column "Description" if we are in Zwischensummenmode
211
  if (not defined $form->{"l_parts"}) {
212
    $form->{"l_description"} = "Y";
213
  };
160 214
  map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
161 215

  
162

  
163 216
  my @options;
217

  
164 218
  if ($form->{description}) {
165 219
    push @options, $locale->text('Description') . " : $form->{description}";
166 220
  }
......
170 224
  if ($form->{customernumber}) {
171 225
    push @options, $locale->text('Customer Number') . " : $form->{customernumber}";
172 226
  }
227
# TODO: es wird nur id übergeben
173 228
  if ($form->{department}) {
174 229
    my ($department) = split /--/, $form->{department};
175 230
    push @options, $locale->text('Department') . " : $department";
......
183 238
  if ($form->{partnumber}) {
184 239
    push @options, $locale->text('Part Number') . " : $form->{partnumber}";
185 240
  }
241
  if ($form->{partsgroup_id}) {
242
    my $partsgroup = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
243
    push @options, $locale->text('Group') . " : $partsgroup->{partsgroup}";
244
  }
245
  if ($form->{country}) {
246
    push @options, $locale->text('Country') . " : $form->{country}";
247
  }
248
  if ($form->{employee_id}) {
249
    my $employee = SL::DB::Employee->new(id => $form->{employee_id})->load;
250
    push @options, $locale->text('Employee') . ' : ' . $employee->name;
251
  }
252
  if ($form->{salesman_id}) {
253
    my $salesman = SL::DB::Employee->new(id => $form->{salesman_id})->load;
254
    push @options, $locale->text('Salesman') . ' : ' . $salesman->name;
255
  }
256
  if ($form->{business_id}) {
257
    my $business = SL::DB::Business->new(id => $form->{business_id})->load;
258
    push @options, $locale->text('Customer type') . ' : ' . $business->description;
259
  }
186 260
  if ($form->{ordnumber}) {
187 261
    push @options, $locale->text('Order Number') . " : $form->{ordnumber}";
188 262
  }
......
201 275

  
202 276
  my $report = SL::ReportGenerator->new(\%myconfig, $form);
203 277

  
204
  $report->set_options('top_info_text'        => join("\n", @options),
278
  $report->set_options('top_info_text'        => join("\n", $locale->text('Main sorting') . ' : ' . $column_defs{$form->{mainsort}}->{text} , $locale->text('Secondary sorting') . ' : ' . $column_defs{$form->{'subsort'}}->{text}, @options),
205 279
                       'output_format'        => 'HTML',
206 280
                       'title'                => $form->{title},
207 281
                       'attachment_basename'  => $locale->text('Sales Report') . strftime('_%Y%m%d', localtime time),
......
216 290

  
217 291
  $report->set_sort_indicator($form->{mainsort}, $form->{sortdir});
218 292

  
293
  CVar->add_custom_variables_to_report('module'         => 'CT',
294
      'trans_id_field' => 'customerid',
295
      'configs'        => $cvar_configs_ct,
296
      'column_defs'    => \%column_defs,
297
      'data'           => $form->{AR}
298
  );
299

  
300
  CVar->add_custom_variables_to_report('module'         => 'IC',
301
      'trans_id_field' => 'parts_id',
302
      'configs'        => $cvar_configs_ic,
303
      'column_defs'    => \%column_defs,
304
      'data'           => $form->{AR}
305
  );
306

  
219 307
  # add sort and escape callback, this one we use for the add sub
220 308
  $form->{callback} = $href .= "&sort=$form->{mainsort}";
221 309

  
......
241 329
    # discount was already accounted for in db sellprice
242 330
    $ar->{sellprice} = $ar->{sellprice} / $ar->{price_factor};
243 331
    $ar->{lastcost} = $ar->{lastcost} / $ar->{price_factor};
244
    $ar->{sellprice_total} = $ar->{qty} * ( $ar->{fxsellprice} * ( 1 - $ar->{discount} ) ) ;
332
    $ar->{sellprice_total} = $ar->{qty} * $ar->{sellprice};
245 333
    $ar->{lastcost_total}  = $ar->{qty} * $ar->{lastcost};
246 334
    # marge_percent wird neu berechnet, da Wert in invoice leer ist (Bug)
247
    $ar->{marge_percent} = $ar->{sellprice_total} ? (($ar->{sellprice_total}-$ar->{lastcost_total}) / $ar->{sellprice_total}) : 0;
335
    $ar->{marge_percent} = $ar->{sellprice_total} ? (($ar->{sellprice_total}-$ar->{lastcost_total}) / $ar->{sellprice_total} * 100) : 0;
248 336
    # marge_total neu berechnen
249 337
    $ar->{marge_total} = $ar->{sellprice_total} ? $ar->{sellprice_total}-$ar->{lastcost_total}  : 0;
250 338
    $ar->{discount} *= 100;  # für Ausgabe formatieren, 10% stored as 0.1 in db
251 339

  
252 340
    # Anfangshauptüberschrift
253
    if ( $form->{l_headers} eq "Y" && ( $idx == 0 or $ar->{ $form->{'mainsort'} } ne $form->{AR}->[$idx - 1]->{ $form->{'mainsort'} } )) {
254
      my $name;
341
    if ( $form->{l_headers_mainsort} eq "Y" && ( $idx == 0 or $ar->{ $form->{'mainsort'} } ne $form->{AR}->[$idx - 1]->{ $form->{'mainsort'} } )) {
255 342
      my $headerrow;
256
      if ( $form->{mainsort} eq 'parts_id' ) {
257
        $headerrow->{description}->{data} = "$ar->{description}";
343

  
344
      # use $emptyname for mainsort header if mainsort is empty
345
      if ( $ar->{$form->{'mainsort'}} ) {
346
        $headerrow->{description}->{data} = $ar->{$form->{'mainsort'}};
258 347
      } else {
259
        $headerrow->{description}->{data} = "$ar->{name}";
348
        $headerrow->{description}->{data} = $locale->text('empty');
260 349
      };
350

  
261 351
      $headerrow->{description}->{class} = "listmainsortheader";
262 352
      my $headerrow_set = [ $headerrow ];
263 353
      $report->add_data($headerrow_set);
......
275 365
      or $ar->{ $form->{'mainsort'} } ne $form->{AR}->[$idx - 1]->{ $form->{'mainsort'} }
276 366
    ) {
277 367
      my $headerrow;
278
      my $name;
279
      if ( $form->{subsort} eq 'parts_id' ) {
280
        $name = 'description';
281
        $headerrow->{description}->{data} = "$ar->{$name}";
368

  
369
      # if subsort name is defined, use that name in header, otherwise use $emptyname
370
      if ( $ar->{$form->{'subsort'}} ) {
371
        $headerrow->{description}->{data} = $ar->{$form->{'subsort'}};
282 372
      } else {
283
        $name = 'name';
284
        $headerrow->{description}->{data} = "$ar->{$name}";
373
        $headerrow->{description}->{data} = $locale->text('empty');
285 374
      };
286 375
      $headerrow->{description}->{class} = "listsubsortheader";
287 376
      my $headerrow_set = [ $headerrow ];
288
      $report->add_data($headerrow_set) if $form->{l_headers} eq "Y";
377
      # special case: subsort headers only makes (aesthetical) sense if we show individual parts
378
      $report->add_data($headerrow_set) if $form->{l_headers_subsort} eq "Y" and $form->{l_parts};
289 379
    };
290 380

  
291 381
    map { $subtotals1{$_} += $ar->{$_};
......
318 408
    };
319 409

  
320 410
    # Ertrag prozentual in den Summen: (summe VK - summe Ertrag) / summe VK
321
    $subtotals1{marge_percent} = $subtotals1{sellprice_total} ? (($subtotals1{sellprice_total} - $subtotals1{lastcost_total}) / $subtotals1{sellprice_total}) : 0;
322
    $subtotals2{marge_percent} = $subtotals2{sellprice_total} ? (($subtotals2{sellprice_total} - $subtotals2{lastcost_total}) / $subtotals2{sellprice_total}) : 0;
411
    $subtotals1{marge_percent} = $subtotals1{sellprice_total} ? (($subtotals1{sellprice_total} - $subtotals1{lastcost_total}) / $subtotals1{sellprice_total}) * 100 : 0;
412
    $subtotals2{marge_percent} = $subtotals2{sellprice_total} ? (($subtotals2{sellprice_total} - $subtotals2{lastcost_total}) / $subtotals2{sellprice_total}) *100 : 0;
323 413

  
324 414
    # Ertrag prozentual:  (Summe VK betrag - Summe EK betrag) / Summe VK betrag
325 415
    # wird laufend bei jeder Position neu berechnet
326
    $totals{marge_percent}    = $totals{sellprice_total}    ? ( ($totals{sellprice_total} - $totals{lastcost_total}) / $totals{sellprice_total}   ) : 0;
416
    $totals{marge_percent}    = $totals{sellprice_total}    ? ( ($totals{sellprice_total} - $totals{lastcost_total}) / $totals{sellprice_total}   ) * 100 : 0;
327 417

  
328 418
    map { $ar->{$_} = $form->format_amount(\%myconfig, $ar->{$_}, 2) } qw(marge_total marge_percent);
329 419
    map { $ar->{$_} = $form->format_amount(\%myconfig, $ar->{$_}, $form->{"decimalplaces"} )} qw(lastcost sellprice sellprice_total lastcost_total);
......
342 432
    $row->{invnumber}->{link} = build_std_url("script=is.pl", 'action=edit')
343 433
      . "&id=" . E($ar->{id}) . "&callback=${callback}";
344 434

  
345
    my $row_set = [ $row ];
435
    # Einzelzeilen nur zeigen wenn l_parts gesetzt ist, nützlich, wenn man nur
436
    # Subtotals und Totals sehen möchte
437
    my $row_set = $form->{l_parts} ? [ $row ] : [ ];
346 438

  
347
    if (($form->{l_subtotal} eq 'Y')
439
    # hier wird bei l_subtotal nicht differenziert zwischen mainsort und subsort
440
    # macht man l_subtotal_mainsort aus wird l_subtotal_subsort auch nicht ausgeführt
441
    if (($form->{l_subtotal_mainsort} eq 'Y')
348 442
        && (($idx == (scalar @{ $form->{AR} } - 1))   # last element always has a subtotal
349 443
          || ($ar->{ $form->{'subsort'} } ne $form->{AR}->[$idx + 1]->{ $form->{'subsort'}   })
350 444
          || ($ar->{ $form->{'mainsort'} } ne $form->{AR}->[$idx + 1]->{ $form->{'mainsort'} })
351 445
          )) {   # if value that is sorted by changes, print subtotal
352
      my $name;
353
      if ( $form->{subsort} eq 'parts_id' ) {
354
        $name = 'description';
355
      } else {
356
        $name = 'name';
357
      };
358 446

  
359
      if ($form->{l_subtotal} eq 'Y') {
360
        push @{ $row_set }, create_subtotal_row_invoice(\%subtotals2, \@columns, \%column_alignment, \@subtotal_columns, 'listsubsortsubtotal', $ar->{$name}) ;
361
        push @{ $row_set }, insert_empty_row();
447
      if ($form->{l_subtotal_subsort} eq 'Y') {
448
        push @{ $row_set }, create_subtotal_row_invoice(\%subtotals2, \@columns, \%column_alignment, \@subtotal_columns, 'listsubsortsubtotal', $ar->{ $form->{'subsort'} }) ;
449
        push @{ $row_set }, insert_empty_row() if $form->{l_parts} and $addemptylines;
362 450
      };
363 451
    }
364 452

  
365
    # if mainsort has changed, add mainsort subtotal and empty row
366
    if (($form->{l_subtotal} eq 'Y')
453
    # if last mainsort is reached or mainsort has changed, add mainsort subtotal and empty row
454
    if (($form->{l_subtotal_mainsort} eq 'Y')
367 455
        && (($idx == (scalar @{ $form->{AR} } - 1))   # last element always has a subtotal
368 456
            || ($ar->{ $form->{'mainsort'} } ne $form->{AR}->[$idx + 1]->{ $form->{'mainsort'} })
369 457
            )) {   # if value that is sorted by changes, print subtotal
370
      my $name;
371
      if ( $form->{mainsort} eq 'parts_id' ) {
372
        $name = 'description';
373
      } else {
374
        $name = 'name';
375
      };
376
      if ($form->{l_subtotal} eq 'Y' ) {
377
        push @{ $row_set }, create_subtotal_row_invoice(\%subtotals1, \@columns, \%column_alignment, \@subtotal_columns, 'listmainsortsubtotal', $ar->{$name});
378
        push @{ $row_set }, insert_empty_row();
458
      if ($form->{l_subtotal_mainsort} eq 'Y' and $form->{mainsort} ne $form->{subsort} ) {
459
        # subtotal is overriden if mainsort and subsort are equal, don't print
460
        # subtotal line even if it is selected
461
        push @{ $row_set }, create_subtotal_row_invoice(\%subtotals1, \@columns, \%column_alignment, \@subtotal_columns, 'listmainsortsubtotal', $ar->{$form->{mainsort}});
462
        push @{ $row_set }, insert_empty_row() if $addemptylines; # insert empty row after mainsort
379 463
      };
380 464
    }
381 465

  
......
385 469
  }
386 470
  if ( $form->{l_total} eq "Y" ) {
387 471
    $report->add_separator();
388
    $report->add_data(create_subtotal_row_invoice(\%totals, \@columns, \%column_alignment, \@total_columns, 'listtotal'))
472
    $report->add_data(create_subtotal_row_invoice(\%totals, \@columns, \%column_alignment, \@total_columns, 'listtotal', 'l_total'))
389 473
  };
390 474

  
391 475
  $report->generate_with_headers();
......
409 493

  
410 494
  my $form     = $main::form;
411 495
  my %myconfig = %main::myconfig;
496
  my $locale   = $main::locale;
412 497

  
413 498
  my $row = { map { $_ => { 'data' => '', 'class' => $class, 'align' => $column_alignment->{$_}, } } @{ $columns } };
414 499

  
415
  $row->{description}->{data} = "Summe " . $name;
500
  # set name as "empty" if no value is given, except if we are dealing with the
501
  # absolute total, then just write "Total sum"
502
  # here we assume that no name will be called 'l_total'
503
  $name = $locale->text('empty') unless $name;
504
  if ( $name eq 'l_total' ) {
505
    $row->{description}->{data} = $locale->text('Total sum');
506
  } else {
507
    $row->{description}->{data} = $locale->text('Total') . ' ' . $name;
508
  };
416 509

  
417 510
  map { $row->{$_}->{data} = $form->format_amount(\%myconfig, $totals->{$_}, 2) } qw(marge_total marge_percent);
418 511
  map { $row->{$_}->{data} = $form->format_amount(\%myconfig, $totals->{$_}, 0) } qw(qty);

Auch abrufbar als: Unified diff