App-InvestSim

 view release on metacpan or  search on metacpan

lib/App/InvestSim/GUI.pm  view on Meta::CPAN

my @core_display_values;
my $display_table;
my $rent_cap_entry;

# Some variables that are linked to widgets, that are set from elsewhere.
my ($total_rent_text, $rent_cap_text, $pinel_worth_text, $notary_fees_text, $total_invested_text);

# How to format the various modes of the drop-down menu.
my @modes_format;

# Add in the given frame in column 0 and 1 a label and an entry text box in the
# given row that is incremented ($row is a ref to a scalar).
sub add_input_entry {
  my ($frame, $row, $key, $text, $format, $tooltip) = @_;
  my (undef, $validate) = @{$values_config{$key}};
  my $var_ref = \$values{$key};
  $frame->new_ttk__label(-text => "${text} :")
    ->g_grid(-column => 0, -row => $$row, -sticky => "e", -padx => "0 2");
  my $e = $frame->new_ttk__entry(-width => ENTRY_WIDTH);
    $e->g_grid(-column => 1, -row => $$row++, -sticky => "we", -pady => 2);
  setup_entry($e, $var_ref, $format, $validate);
  if ($tooltip) {
    local $Text::Wrap::columns = 50;
    $e->g_tooltip__tooltip(Text::Wrap::fill('', '', $tooltip)); 
  }
}

# Build the main window of the app.
sub build {

lib/App/InvestSim/GUI.pm  view on Meta::CPAN

  
  # We're copying the font used by the TreeView style and adding an 'bold'
  # option to it, it will be used by the 'total' line.
  my $default_treeview_font = Tkx::ttk__style('lookup', 'TreeView', '-font');
  my $treeview_total_font = Tkx::font('create');
  Tkx::font('configure', $treeview_total_font, Tkx::SplitList(Tkx::font('configure', $default_treeview_font)));
  Tkx::font('configure', $treeview_total_font, -weight => 'bold');
  
  # Build the left bar with various parameters.
  {
    my $frame = $root->new_ttk__frame(-padding => 3);
    $frame->g_grid(-column => 0, -row => 0, -rowspan => 3, -sticky => "we");
    
    my $row = 0;
    for my $c (['invested', "Valeur du bien", \&format_euro, "Prix d'achat du bien, hors frais de notaire."],
               ['tax_rate', "Taux d'imposition marginal", \&format_percent],
               ['base_rent', "Loyer brut initial", \&format_euro],
               ['rent_charges', "Charge et gestion locative", \&format_percent],
               ['rent_increase', "revalorisation loyer", \&format_percent],
               ['duration', "Durée d'investissement", \&format_year],
               ['notary_fees', "Frais de notaire", \&format_percent],
               ['loan_insurance', "Assurance décès du prêt", \&format_percent],
               ['other_rate', "Taux de placement autre", \&format_percent],
               ['surface', "Superficie (pondérée)", \&format_surface, "Surface totale habitable, additionnée, le cas échéant, de la moitié des surfaces annexes dans la limite de 8m² (utilisée seulement pour l'application des plafonds 'Pinel')."]) {
      add_input_entry($frame, \$row, @$c);
    }
  }

  # Build the top bar with the loan duration and rate values.
  my @loan_duration_texts;  # Re-used in the core data table.
  {
    my $frame = $root->new_ttk__frame(-padding => 3);
    $frame->g_grid(-column => 1, -row => 0, -sticky => "nwes");
    $frame->g_grid_columnconfigure(0, -weight => 1); 
      
    $frame->new_ttk__label(-text => "Durée d'emprunt (années)")
      ->g_grid(-column => 0, -row => 0, -sticky => "e");
    $frame->new_ttk__label(-text => "Taux d'emprunt")
      ->g_grid(-column => 0, -row => 1, -sticky => "e");
    my $loan_durations = $values{loan_durations};
    my $validate_duration = $values_config{loan_durations}[1];
    my $loan_rates = $values{loan_rates};
    my $validate_rate = $values_config{loan_rates}[1];
    for my $i (0..NUM_LOAN_DURATION-1) {
      my $d = $frame->new_ttk__entry(-width => ENTRY_WIDTH);
      $d->g_grid(-column => $i + 1, -row => 0, -sticky => "we");
      setup_entry($d, \$loan_durations->[$i], \&format_year, $validate_duration, \$loan_duration_texts[$i]);
      my $r = $frame->new_ttk__entry(-width => ENTRY_WIDTH);
      $r->g_grid(-column => $i + 1, -row => 1, -sticky => "we");
      setup_entry($r, \$loan_rates->[$i], \&format_percent, $validate_rate);
    }
  }

  # Build the combo-box with the list of possible values, in its own frame.
  my @modes;
  $modes[MONTHLY_PAYMENT] = "Mensualité de l'emprunt (assurance comprise)";
  $modes_format[MONTHLY_PAYMENT] = \&format_euro;
  $modes[LOAN_COST] = "Cout total de l'emprunt (assurance comprise)";
  $modes_format[LOAN_COST] = \&format_euro;
  $modes[YEARLY_RENT_AFTER_LOAN] = "Revenus locatif net déduit des remboursement, par an";
  $modes_format[YEARLY_RENT_AFTER_LOAN] = \&format_euro;
  $modes[MEAN_BALANCE_LOAN_DURATION] = "Balance mensuelle moyenne de l'opération sur la durée du prêt";
  $modes_format[MEAN_BALANCE_LOAN_DURATION] = \&format_euro;
  $modes[MEAN_BALANCE_OVERALL] = "Balance mensuelle moyenne de l'opération sur la durée de simulation";
  $modes_format[MEAN_BALANCE_OVERALL] = \&format_euro;
  $modes[NET_GAIN] = "Gain Net de l'opération";
  $modes_format[NET_GAIN] = \&format_euro;
  $modes[INVESTMENT_RETURN] = "Rendement de l'opération";
  $modes_format[INVESTMENT_RETURN] = \&format_percent;
  {
    my $frame = $root->new_ttk__frame(-padding => 5);
    $frame->g_grid(-column => 1, -row => 1, -sticky => "nwes");
    $frame->g_grid_columnconfigure(0, -weight => 1); # So that it extends to the whole width.

    $modes_combobox = $frame->new_ttk__combobox(-state => 'readonly', -values => \@modes, -justify => 'center');
    $modes_combobox->g_grid(-column => 0, -row => 0, -sticky => "we");
    $modes_combobox->m_current(MONTHLY_PAYMENT);
    $modes_combobox->g_bind('<<ComboboxSelected>>', \&update_displayed_mode);
  }

  # Build the core table with the computation output.
  {
    my $frame = $root->new_ttk__frame(-padding => 3);
    $frame->g_grid(-column => 1, -row => 2, -sticky => "nwes");
     # So that it extends to the same width as column 0 of the top bar. All other
     # columns have a fixed width that is the same as the matching column in the
     # top bar.
    $frame->g_grid_columnconfigure(0, -weight => 1);

    $frame->new_ttk__label(-text => 'Emprunt \ Durée')
      ->g_grid(-column => 0, -row => 0);
    for my $i (0..NUM_LOAN_DURATION-1) {
      $frame->new_ttk__entry(-width => ENTRY_WIDTH, -textvariable => \$loan_duration_texts[$i], -state => 'readonly', -takefocus => 0)
        ->g_grid(-column => $i + 1, -row => 0, -sticky => "we");
    }
    my $loan_amounts = $values{loan_amounts};
    my $validate_amount = $values_config{loan_amounts}[1];
    for my $j (0..NUM_LOAN_AMOUNT-1) {
      my $w = $frame->new_ttk__entry(-width => ENTRY_WIDTH, -justify => 'right');
      $w->g_grid(-column => 0, -row => $j + 1, -sticky => "we");
      setup_entry($w, \$loan_amounts->[$j], \&format_euro, $validate_amount);
    }
    for my $i (0..NUM_LOAN_DURATION-1) {
      for my $j (0..NUM_LOAN_AMOUNT-1) {
        my $e = $frame->new_ttk__entry(-width => ENTRY_WIDTH, -textvariable => \$core_display_values[$i][$j],
                                       -state => 'readonly', -justify => 'right', -takefocus => 0,
                                       -style => 'DataTable.TEntry');
        $e->g_grid(-column => $i + 1, -row => $j + 1, -sticky => "we");
        $e->g_bind('<FocusIn>', sub { set_core_table_selected_state($e);
                                      update_displayed_table($i, $j) });
      }
    }
  }

  # Build the right bar with some other input values and the output values not 
  # depending on the loan parameters.
  {
    my $frame = $root->new_ttk__frame(-padding => 3);
    $frame->g_grid(-column => 2, -row => 0, -rowspan => 3, -sticky => "we");
    
    my $row = 0;
    
    for my $c (['rent_delay', "Delai de mise en location", \&format_year],
               ['loan_delay', "Durée de franchise de l'emprunt", \&format_year],
               ['application_fees', "Frais de dossier du prêt", \&format_euro],
               ['mortgage_fees', "Frais d'hypothèque", \&format_percent],
               ['social_tax', "CSG + CRDS + Solidarité", \&format_percent]) {
      add_input_entry($frame, \$row, @$c);
    }
    
    # Just some empty white-space between the inputs and the output fields.
    $frame->g_grid_rowconfigure($row++, -minsize => 10);
    
    $frame->new_ttk__label(-text => "Revenus total du loyer (net) :")
      ->g_grid(-column => 0, -row => $row, -sticky => "e", -padx => "0 2");
    $frame->new_ttk__entry(-width => ENTRY_WIDTH, -state => 'readonly', -textvariable => \$total_rent_text, -takefocus => 0)
      ->g_grid(-column => 1, -row => $row++, -sticky => "we", -pady => 2);

    $frame->new_ttk__label(-text => "Plafond du loyer :")
      ->g_grid(-column => 0, -row => $row, -sticky => "e", -padx => "0 2");
    ($rent_cap_entry = $frame->new_ttk__entry(-width => ENTRY_WIDTH, -state => 'readonly', -textvariable => \$rent_cap_text, -takefocus => 0))
      ->g_grid(-column => 1, -row => $row++, -sticky => "we", -pady => 2);

      $frame->new_ttk__label(-text => "Valeur déductible du bien :")
      ->g_grid(-column => 0, -row => $row, -sticky => "e", -padx => "0 2");
    $frame->new_ttk__entry(-width => ENTRY_WIDTH, -state => 'readonly', -textvariable => \$pinel_worth_text, -takefocus => 0)
      ->g_grid(-column => 1, -row => $row++, -sticky => "we", -pady => 2);

    $frame->new_ttk__label(-text => "Frais de notaire :")
      ->g_grid(-column => 0, -row => $row, -sticky => "e", -padx => "0 2");
    $frame->new_ttk__entry(-width => ENTRY_WIDTH, -state => 'readonly', -textvariable => \$notary_fees_text, -takefocus => 0)
      ->g_grid(-column => 1, -row => $row++, -sticky => "we", -pady => 2);

      $frame->new_ttk__label(-text => "Montant total investi :")
      ->g_grid(-column => 0, -row => $row, -sticky => "e", -padx => "0 2");
    $frame->new_ttk__entry(-width => ENTRY_WIDTH, -state => 'readonly', -textvariable => \$total_invested_text, -takefocus => 0)
      ->g_grid(-column => 1, -row => $row++, -sticky => "we", -pady => 2);
  }

  # Build the bottom table.
  $root->g_grid_columnconfigure(1, -weight => 1);
  $root->g_grid_rowconfigure(3, -weight => 1);
  {
    my $frame = $root->new_ttk__frame(-padding => 3, -height => 550);
    $frame->g_grid_propagate(0);
    $frame->g_grid(-column => 0, -row => 3, -columnspan => 3, -sticky => "nwes");
    $frame->g_grid_columnconfigure(0, -weight => 1);
    $frame->g_grid_rowconfigure(0, -weight => 1);
    $display_table = $frame->new_ttk__treeview(-height => ($values{duration} // 20) + 2);
    $display_table->g_grid(-column => 0, -row => 0, -rowspan => 2, -sticky => "nwes");
    
    # We're setting a specific font for items with the tag 'total'.
    $display_table->m_tag('configure', 'total', -font => $treeview_total_font);
    
    #my $hscroll = $frame->new_tk__scrollbar(-orient => "horizontal", -command => [$display_table, "xview"]);
    #$hscroll->g_grid(-column => 0, -row => 1, -sticky => "we");
    my $vscroll = $frame->new_ttk__scrollbar(-orient => "vertical", -command => [$display_table, "yview"]);
    $vscroll->g_grid(-column => 1, -row => 0, -sticky => "ns");
    $frame->new_ttk__sizegrip()->g_grid(-column => 1, -row => 1, -sticky => "se");
    $display_table->configure(-yscrollcommand => [$vscroll, "set"]);
    #$display_table->configure(-xscrollcommand => [$hscroll, "set"]);
    
    my @headings = ('Année', 'Loyer net', 'Placements', 'Principal du prêt', 'Intérêts du prêt', 'Frais du prêt', 'Revenus imposable', 'Déficit déductible', 'Impôt', 'Solde annuel', 'Capital');
    # We're not using the name of the columns (c1, c2, ...) we're only using their
    # index (#0, #1, ...), including #0 the index of the first implicit column.
    $display_table->m_configure(-columns => [map { "c$_" } 1..$#headings ]);
    for my $c (0..$#headings) {
      my $width = Tkx::font_measure(Tkx::ttk__style_lookup('Heading', '-font'), $headings[$c]);
      $display_table->m_heading("#${c}", -text => $headings[$c]);

lib/App/InvestSim/GUI.pm  view on Meta::CPAN

  if ($values{automatic_duration} && $values{loan_durations}[$d] != $values{duration}) {
    $values{duration} = $values{loan_durations}[$d];
    refresh_all_fields();
    return; # This method is called back by refresh_all_fields() through calculate_all().
  }
  clear_displayed_table();
  # We don't configure the height of the table unconditionnally because doing so
  # will slightly change the width of the widget for some weird reason. Forcing
  # the width does not work well (and yield some ugly redraw), so we don't do it.
  # All this is unused anyway now that we have verticall scrolling and a set size
  # for the enclosing frame.
  my $current_height = (Tkx::SplitList($display_table->m_configure('-height')))[-1];
  $display_table->m_configure(-height => $values{duration} || 1) if $values{duration} != $current_height;
  my $table = $computed_values[$d][$a][TABLE_DATA];
  for my $i (0..$#$table) {
    my $text = $i ? $i : 'Achat';
    $display_table->m_insert('', 'end', -text => $text, -values => [ map { format_euro($_) } @{$table->[$i]}]);
  }
  $display_table->m_insert('', 'end', -text => 'Total', -tags => 'total',
                           -values => [ (map { format_euro($_) } @{$computed_values[$d][$a][TABLE_TOTAL]}), '']);
}



( run in 1.637 second using v1.01-cache-2.11-cpan-e1769b4cff6 )