diff --git a/ucalc/BillingWindow.xaml b/ucalc/BillingWindow.xaml
index 333cd37..88c4e18 100644
--- a/ucalc/BillingWindow.xaml
+++ b/ucalc/BillingWindow.xaml
@@ -61,6 +61,7 @@
diff --git a/ucalc/BillingWindow.xaml.cs b/ucalc/BillingWindow.xaml.cs
index 09b1f2e..2209a42 100644
--- a/ucalc/BillingWindow.xaml.cs
+++ b/ucalc/BillingWindow.xaml.cs
@@ -132,6 +132,12 @@ namespace UCalc
page.ParentWindow = this;
}
+ private void OnDetailsFrameLoadCompleted(object sender, NavigationEventArgs e)
+ {
+ var page = (DetailsPage) ((Frame) sender).Content;
+ page.Model = Model;
+ }
+
public bool Save(bool rename = false)
{
if (Model.Root.Errors.Count > 0)
@@ -195,7 +201,7 @@ namespace UCalc
private void OnDetailsTabSelected(object sender, RoutedEventArgs e)
{
- ((DetailsPage) DetailsFrame.Content).Compute(Model);
+ ((DetailsPage) DetailsFrame.Content).Compute();
}
}
}
\ No newline at end of file
diff --git a/ucalc/Controls/Converters.cs b/ucalc/Controls/Converters.cs
index e777fd4..e9a2a61 100644
--- a/ucalc/Controls/Converters.cs
+++ b/ucalc/Controls/Converters.cs
@@ -108,28 +108,6 @@ namespace UCalc.Controls
}
}
- public class DatePickerTextToDateTimeConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- var dateTime = (DateTime?) value;
-
- return dateTime == null ? "" : dateTime.Value.ToString(Constants.DateFormat);
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- var str = (string) value;
-
- if (DateTime.TryParseExact(str, Constants.DateFormat, null, DateTimeStyles.None, out var d))
- {
- return d;
- }
-
- return null;
- }
- }
-
public class NameToTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/ucalc/Controls/Helpers.cs b/ucalc/Controls/Helpers.cs
index bc974e7..d563615 100644
--- a/ucalc/Controls/Helpers.cs
+++ b/ucalc/Controls/Helpers.cs
@@ -28,7 +28,7 @@ namespace UCalc.Controls
}
}
- private static bool IsBetween(this DateTime dt, DateTime start, DateTime end)
+ public static bool IsBetween(this DateTime dt, DateTime start, DateTime end)
{
return dt >= start && dt <= end;
}
@@ -42,5 +42,25 @@ namespace UCalc.Controls
{
return $"0.{new string('0', precision - optional)}{new string('#', optional)}";
}
+
+ public static decimal Ceil2(this decimal d, int precision = Constants.DisplayPrecision)
+ {
+ return Math.Ceiling(d * (decimal) Math.Pow(10, precision)) / (decimal) Math.Pow(10, precision);
+ }
+
+ public static string CeilToString(this decimal d, int precision = Constants.DisplayPrecision, int optional = 0)
+ {
+ return Ceil2(d).ToString(PrecisionToFormat(precision, optional));
+ }
+
+ public static string CeilAmountToString(this decimal d, int precision = Constants.DisplayPrecision)
+ {
+ return d.CeilToString(precision, precision - 2);
+ }
+
+ public static string CeilUnitCountToString(this decimal d, int precision = Constants.DisplayPrecision)
+ {
+ return d.CeilToString(precision, precision - 3);
+ }
}
}
\ No newline at end of file
diff --git a/ucalc/CostWindow.xaml b/ucalc/CostWindow.xaml
index e00efc2..d6a4786 100644
--- a/ucalc/CostWindow.xaml
+++ b/ucalc/CostWindow.xaml
@@ -18,7 +18,6 @@
-
@@ -120,7 +119,7 @@
Foreground="{x:Static local:Constants.SubMainColor}" />
+ IsChecked="{Binding Path=Cost.ShiftUnrented.Value, RelativeSource={RelativeSource AncestorType=local:CostWindow}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
@@ -246,9 +245,7 @@
+ SelectedDate="{Binding Path=StartDate.Value, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
@@ -264,9 +261,7 @@
+ SelectedDate="{Binding Path=EndDate.Value, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
diff --git a/ucalc/Data/Billing.cs b/ucalc/Data/Billing.cs
index d7604bb..73d39a9 100644
--- a/ucalc/Data/Billing.cs
+++ b/ucalc/Data/Billing.cs
@@ -472,8 +472,8 @@ namespace UCalc.Data
[JsonProperty(PropertyName = "affectsAll"), JsonRequired]
public bool AffectsAll { get; set; }
- [JsonProperty(PropertyName = "includeUnrented"), JsonRequired]
- public bool IncludeUnrented { get; set; }
+ [JsonProperty(PropertyName = "shiftUnrented"), JsonRequired]
+ public bool ShiftUnrented { get; set; }
[JsonProperty(PropertyName = "affectedFlats"), JsonRequired, JsonConverter(typeof(FlatSerializationConverter))]
public HashSet AffectedFlats { get; set; }
@@ -494,7 +494,7 @@ namespace UCalc.Data
private bool Equals(Cost other)
{
return Name == other.Name && Division == other.Division && AffectsAll == other.AffectsAll &&
- IncludeUnrented == other.IncludeUnrented && AffectedFlats.SequenceEqual(other.AffectedFlats) &&
+ ShiftUnrented == other.ShiftUnrented && AffectedFlats.SequenceEqual(other.AffectedFlats) &&
Entries.SequenceEqual(other.Entries) && DisplayInBill == other.DisplayInBill;
}
@@ -512,7 +512,7 @@ namespace UCalc.Data
Name = Name,
Division = Division,
AffectsAll = AffectsAll,
- IncludeUnrented = IncludeUnrented,
+ ShiftUnrented = ShiftUnrented,
AffectedFlats = new HashSet(AffectedFlats.Select(flat => flatMapper[flat])),
Entries = new List(Entries.Select(entry => entry.Clone())),
DisplayInBill = DisplayInBill
diff --git a/ucalc/Data/BillingCalculator.cs b/ucalc/Data/BillingCalculator.cs
new file mode 100644
index 0000000..2b9c8e8
--- /dev/null
+++ b/ucalc/Data/BillingCalculator.cs
@@ -0,0 +1,631 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UCalc.Controls;
+
+namespace UCalc.Data
+{
+ public class TenantCalculationResult
+ {
+ public Tenant Tenant { get; }
+ public IReadOnlyDictionary Costs { get; }
+ public decimal TotalAmount { get; }
+ public string Details { get; }
+ public string DetailsForLandlord { get; }
+
+ public TenantCalculationResult(Tenant tenant, IReadOnlyDictionary costs,
+ decimal totalAmount, string details, string detailsForLandlord)
+ {
+ Tenant = tenant;
+ Costs = costs;
+ TotalAmount = totalAmount;
+ Details = details;
+ DetailsForLandlord = detailsForLandlord;
+ }
+ }
+
+ public class CostCalculationResult
+ {
+ public decimal TotalAmount { get; }
+ public string Details { get; }
+ public string DetailsForLandlord { get; }
+ public bool AffectsTenant { get; }
+
+ public CostCalculationResult(decimal totalAmount, string details, string detailsForLandlord, bool affectsTenant)
+ {
+ TotalAmount = totalAmount;
+ Details = details;
+ DetailsForLandlord = detailsForLandlord;
+ AffectsTenant = affectsTenant;
+ }
+ }
+
+ public static class BillingCalculator
+ {
+ private class EventCause
+ {
+ public Tenant Tenant { get; }
+ public bool Entry { get; }
+
+ public EventCause(Tenant tenant, bool entry)
+ {
+ Tenant = tenant;
+ Entry = entry;
+ }
+ }
+
+ private class Event
+ {
+ public DateTime Date { get; }
+ public List Causes { get; }
+
+ public string Cause
+ {
+ get
+ {
+ if (Causes.Count == 0)
+ {
+ return null;
+ }
+
+ return string.Join(" / ",
+ Causes.Select(cause =>
+ cause.Entry
+ ? $"Einzug von \"{cause.Tenant.Name}\""
+ : $"Auszug von \"{cause.Tenant.Name}\""));
+ }
+ }
+
+ public Event(DateTime date, List causes)
+ {
+ Date = date;
+ Causes = causes;
+ }
+ }
+
+ private static bool AffectsTenant(Tenant tenant, Cost cost)
+ {
+ return cost.AffectsAll || cost.AffectedFlats.Intersect(tenant.RentedFlats).Any();
+ }
+
+ private static IEnumerable AffectedTenants(Billing billing, Cost cost)
+ {
+ if (cost.AffectsAll)
+ {
+ return billing.Tenants;
+ }
+
+ return billing.Tenants.Where(tenant => tenant.RentedFlats.Any(flat => cost.AffectedFlats.Contains(flat)));
+ }
+
+ private static List CalculateEvents(Billing billing, IEnumerable affectedTenants)
+ {
+ var events = new Dictionary();
+
+ void AddEvent(DateTime date, Tenant tenant, bool entry)
+ {
+ if (events.TryGetValue(date, out var @event))
+ {
+ @event.Causes.Add(new EventCause(tenant, entry));
+ }
+ else
+ {
+ events.Add(date, new Event(date, new List {new EventCause(tenant, entry)}));
+ }
+ }
+
+ foreach (var tenant in affectedTenants)
+ {
+ if (tenant.EntryDate != null && tenant.EntryDate.Value.IsBetween(billing.StartDate, billing.EndDate))
+ {
+ AddEvent(tenant.EntryDate.Value, tenant, true);
+ }
+
+ if (tenant.DepartureDate != null &&
+ tenant.DepartureDate.Value.IsBetween(billing.StartDate, billing.EndDate))
+ {
+ AddEvent(tenant.DepartureDate.Value, tenant, false);
+ }
+ }
+
+ return events.Values.OrderBy(@event => @event.Date).ToList();
+ }
+
+ private static int GetFirstAffectedEvent(DateTime date, IReadOnlyList events)
+ {
+ for (var i = 0; i < events.Count; ++i)
+ {
+ if (events[i].Date > date)
+ {
+ return i;
+ }
+ }
+
+ return events.Count;
+ }
+
+ private static Tuple GetEffectiveCostEntryStartAndEnd(Billing billing, Tenant tenant,
+ CostEntry entry, out bool coversPast, out bool coversFuture)
+ {
+ coversPast = false;
+ coversFuture = false;
+
+ var entryStartDate = entry.StartDate;
+ var entryEndDate = entry.EndDate;
+
+ if (entryEndDate < billing.StartDate || entryStartDate > billing.EndDate)
+ {
+ return null;
+ }
+
+ if (entryStartDate < billing.StartDate)
+ {
+ coversPast = true;
+ entryStartDate = billing.StartDate;
+ }
+
+ if (entryEndDate > billing.EndDate)
+ {
+ coversFuture = true;
+ entryEndDate = billing.EndDate;
+ }
+
+ if (tenant.EntryDate != null)
+ {
+ if (tenant.EntryDate > entryEndDate)
+ {
+ return null;
+ }
+
+ if (tenant.EntryDate > entryStartDate)
+ {
+ entryStartDate = tenant.EntryDate.Value;
+ }
+ }
+
+ if (tenant.DepartureDate != null)
+ {
+ if (tenant.DepartureDate < entryStartDate)
+ {
+ return null;
+ }
+
+ if (tenant.DepartureDate < entryEndDate)
+ {
+ entryEndDate = tenant.DepartureDate.Value;
+ }
+ }
+
+ return new Tuple(entryStartDate, entryEndDate);
+ }
+
+ private static decimal CalculateCostPerDay(CostEntry entry, StringBuilder details)
+ {
+ decimal totalAmount;
+
+ if (entry.Details.TotalAmount != 0)
+ {
+ var amountPerUnit = entry.Details.TotalAmount / entry.Details.UnitCount;
+ details.Append("Gesamtverbrauch = ");
+ details.Append(entry.Details.UnitCount.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³\n");
+ details.Append("Preis pro m³ = ");
+ details.Append(entry.Details.TotalAmount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" € ÷ ");
+ details.Append(entry.Details.UnitCount.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³ ≈ ");
+ details.Append(amountPerUnit.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €/m³\n");
+
+ var t = entry.Details.UnitCount;
+ details.Append("Verbrauch mit Abzügen = ");
+ details.Append(entry.Details.UnitCount.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³");
+
+ foreach (var discountInUnits in entry.Details.DiscountsInUnits)
+ {
+ if (discountInUnits != 0)
+ {
+ details.Append(" - ");
+ details.Append(discountInUnits.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³");
+ }
+
+ t -= discountInUnits;
+ }
+
+ details.Append(" ≈ ");
+ details.Append(t.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³\n");
+
+ totalAmount = t * amountPerUnit;
+ details.Append("Kosten = ");
+ details.Append(t.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m³ · ");
+ details.Append(amountPerUnit.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €/m³ ≈ ");
+ details.Append(totalAmount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €\n");
+ }
+ else
+ {
+ totalAmount = entry.Amount;
+ }
+
+ var costPerDay = totalAmount / ((entry.EndDate - entry.StartDate).Days + 1);
+ details.Append("Kosten pro Tag = ");
+ details.Append(totalAmount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" € ÷ ");
+ details.Append(((entry.EndDate - entry.StartDate).Days + 1).ToString());
+ details.Append(" Tage ≈ ");
+ details.Append(costPerDay.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €\n\n");
+
+ return costPerDay;
+ }
+
+ private static IEnumerable AffectedFlats(this Cost cost, Billing billing)
+ {
+ return cost.AffectsAll ? (IEnumerable) billing.House.Flats : cost.AffectedFlats;
+ }
+
+ private static IEnumerable AffectedFlats(this Cost cost, Billing billing, DateTime spanStartDate,
+ DateTime spanEndDate)
+ {
+ var flats = cost.AffectedFlats(billing);
+
+ if (cost.ShiftUnrented)
+ {
+ flats = flats.Where(flat => flat.IsRented(billing, spanStartDate, spanEndDate));
+ }
+
+ return flats;
+ }
+
+ private static bool IsRentedBy(this Flat flat, Tenant tenant, DateTime spanStartDate, DateTime spanEndDate)
+ {
+ return tenant.RentedFlats.Contains(flat) &&
+ (tenant.EntryDate == null || tenant.EntryDate.Value <= spanEndDate) &&
+ (tenant.DepartureDate == null || tenant.DepartureDate.Value >= spanStartDate);
+ }
+
+ private static bool IsRented(this Flat flat, Billing billing, DateTime spanStartDate, DateTime spanEndDate)
+ {
+ return billing.Tenants.Any(tenant => flat.IsRentedBy(tenant, spanStartDate, spanEndDate));
+ }
+
+ private static int AffectedPersonCount(this Cost cost, Billing billing, DateTime spanStartDate,
+ DateTime spanEndDate)
+ {
+ return cost.AffectedFlats(billing).Select(flat =>
+ billing.Tenants.Select(tenant =>
+ flat.IsRentedBy(tenant, spanStartDate, spanEndDate) ? tenant.PersonCount : 0).Sum()).Sum();
+ }
+
+ private static decimal AffectedSize(this Cost cost, Billing billing, DateTime spanStartDate,
+ DateTime spanEndDate)
+ {
+ return cost.AffectedFlats(billing, spanStartDate, spanEndDate)
+ .Aggregate((decimal) 0, (sum, flat) => sum + flat.Size);
+ }
+
+ private static int AffectedFlatCount(this Cost cost, Billing billing, DateTime spanStartDate,
+ DateTime spanEndDate)
+ {
+ return cost.AffectedFlats(billing, spanStartDate, spanEndDate).Count();
+ }
+
+ private static decimal CalculateCostEntryForTimeSpan(Billing billing, Tenant tenant, Cost cost,
+ StringBuilder details, DateTime spanStartDate, DateTime spanEndDate, string eventCause, decimal costPerDay)
+ {
+ details.Append("Von ");
+ details.Append(spanStartDate.ToString(Constants.DateFormat));
+ details.Append(" bis ");
+ details.Append(spanEndDate.ToString(Constants.DateFormat));
+
+ if (!string.IsNullOrEmpty(eventCause))
+ {
+ details.Append(" (");
+ details.Append(eventCause);
+ details.Append(")");
+ }
+
+ details.Append(":\n");
+
+ decimal amount;
+ switch (cost.Division)
+ {
+ case CostDivision.Person:
+ var totalPersonCount = cost.AffectedPersonCount(billing, spanStartDate, spanEndDate);
+ var personCount = tenant.PersonCount;
+ amount = costPerDay * ((spanEndDate - spanStartDate).Days + 1) / totalPersonCount * personCount;
+
+ details.Append("Zwischensumme = ");
+ details.Append(costPerDay.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" € · ");
+ details.Append(((spanEndDate - spanStartDate).Days + 1).ToString());
+ details.Append(" Tage ÷ ");
+ details.Append(totalPersonCount.ToString());
+ details.Append(" Personen · ");
+ details.Append(personCount.ToString());
+ details.Append(" Personen ≈ ");
+ details.Append(amount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €\n\n");
+
+ return amount;
+ case CostDivision.Flat:
+ var totalFlatCount = cost.AffectedFlatCount(billing, spanStartDate, spanEndDate);
+ var flatCount = tenant.RentedFlats.Count;
+ amount = costPerDay * ((spanEndDate - spanStartDate).Days + 1) / totalFlatCount * flatCount;
+
+ details.Append("Zwischensumme = ");
+ details.Append(costPerDay.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" € · ");
+ details.Append(((spanEndDate - spanStartDate).Days + 1).ToString());
+ details.Append(" Tage ÷ ");
+ details.Append(totalFlatCount.ToString());
+ if (cost.ShiftUnrented)
+ {
+ details.Append(" bewohnte");
+ }
+
+ details.Append(" Wohnungen · ");
+ details.Append(flatCount.ToString());
+ details.Append(" Wohnungen ≈ ");
+ details.Append(amount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €\n\n");
+
+ return amount;
+ case CostDivision.Size:
+ var totalSize = cost.AffectedSize(billing, spanStartDate, spanEndDate);
+ var size = tenant.RentedFlats.Aggregate((decimal) 0, (sum, flat) => sum + flat.Size);
+
+ amount = costPerDay * ((spanEndDate - spanStartDate).Days + 1) / totalSize * size;
+ details.Append("Zwischensumme = ");
+ details.Append(costPerDay.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" € · ");
+ details.Append(((spanEndDate - spanStartDate).Days + 1).ToString());
+ details.Append(" Tage ÷ ");
+ details.Append(totalSize.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m² · ");
+ details.Append(size.CeilUnitCountToString(Constants.InternalPrecision));
+ details.Append(" m² ≈ ");
+ details.Append(amount.CeilAmountToString(Constants.InternalPrecision));
+ details.Append(" €\n\n");
+
+ return amount;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private static decimal CalculateCostEntry(Billing billing, Tenant tenant, Cost cost, CostEntry entry,
+ IReadOnlyList events, StringBuilder details, ICollection pastCoveringEntries,
+ ICollection futureCoveringEntries)
+ {
+ var entryDates =
+ GetEffectiveCostEntryStartAndEnd(billing, tenant, entry, out var coversPast, out var coversFuture);
+
+ if (coversPast)
+ {
+ pastCoveringEntries.Add(entry);
+ }
+
+ if (coversFuture)
+ {
+ futureCoveringEntries.Add(entry);
+ }
+
+ if (entryDates == null)
+ {
+ return 0;
+ }
+
+ var (entryStartDate, entryEndDate) = entryDates;
+ var index = GetFirstAffectedEvent(entryStartDate, events);
+ var costPerDay = CalculateCostPerDay(entry, details);
+
+ decimal totalAmount = 0;
+ var prevDate = entryStartDate;
+
+ while (index < events.Count && events[index].Date <= entryEndDate)
+ {
+ var newDate = events[index].Date;
+
+ if (prevDate != newDate)
+ {
+ totalAmount += CalculateCostEntryForTimeSpan(billing, tenant, cost, details, prevDate,
+ newDate, events[index].Cause, costPerDay);
+ newDate = newDate.AddDays(1);
+ }
+
+ prevDate = newDate;
+ ++index;
+ }
+
+ if (prevDate <= entryEndDate)
+ {
+ totalAmount += CalculateCostEntryForTimeSpan(billing, tenant, cost, details, prevDate,
+ entryEndDate, null, costPerDay);
+ }
+
+ return totalAmount;
+ }
+
+ private static void CalculateCostInPast(Billing billing, Tenant tenant, Cost cost, StringBuilder details,
+ IEnumerable pastCoveringEntries)
+ {
+ details.Append("In vergangener Abrechnung bereits gezahlt (geschätzt) = ");
+ decimal totalAmount = 0;
+ var dummy = new StringBuilder();
+
+ foreach (var entry in pastCoveringEntries)
+ {
+ var entryStartDate = entry.StartDate;
+ var entryEndDate = billing.StartDate.AddDays(-1);
+
+ if (tenant.EntryDate != null)
+ {
+ if (tenant.EntryDate.Value > entryEndDate)
+ {
+ continue;
+ }
+
+ if (tenant.EntryDate.Value > entryStartDate)
+ {
+ entryStartDate = tenant.EntryDate.Value;
+ }
+ }
+
+ if (tenant.DepartureDate != null)
+ {
+ if (tenant.DepartureDate.Value < entryStartDate)
+ {
+ continue;
+ }
+
+ if (tenant.DepartureDate.Value < entryEndDate)
+ {
+ entryEndDate = tenant.DepartureDate.Value;
+ }
+ }
+
+ var costPerDay = CalculateCostPerDay(entry, dummy);
+ totalAmount += CalculateCostEntryForTimeSpan(billing, tenant, cost, dummy, entryStartDate,
+ entryEndDate, null, costPerDay);
+ }
+
+ details.Append(totalAmount.CeilToString());
+ details.Append(" €\n");
+ }
+
+ private static void CalculateCostInFuture(Billing billing, Tenant tenant, Cost cost, StringBuilder details,
+ IEnumerable futureCoveringEntries)
+ {
+ details.Append("In nächster Abrechnung erwartet (geschätzt) = ");
+ decimal totalAmount = 0;
+ var dummy = new StringBuilder();
+
+ foreach (var entry in futureCoveringEntries)
+ {
+ var entryStartDate = billing.EndDate.AddDays(1);
+ var entryEndDate = entry.EndDate;
+
+ if (tenant.EntryDate != null)
+ {
+ if (tenant.EntryDate.Value > entryEndDate)
+ {
+ continue;
+ }
+
+ if (tenant.EntryDate.Value > entryStartDate)
+ {
+ entryStartDate = tenant.EntryDate.Value;
+ }
+ }
+
+ if (tenant.DepartureDate != null)
+ {
+ if (tenant.DepartureDate.Value < entryStartDate)
+ {
+ continue;
+ }
+
+ if (tenant.DepartureDate.Value < entryEndDate)
+ {
+ entryEndDate = tenant.DepartureDate.Value;
+ }
+ }
+
+ var costPerDay = CalculateCostPerDay(entry, dummy);
+ totalAmount += CalculateCostEntryForTimeSpan(billing, tenant, cost, dummy, entryStartDate,
+ entryEndDate, null, costPerDay);
+ }
+
+ details.Append(totalAmount.CeilToString());
+ details.Append(" €\n");
+ }
+
+ private static CostCalculationResult CalculateCost(Billing billing, Tenant tenant, Cost cost)
+ {
+ if (!AffectsTenant(tenant, cost))
+ {
+ return new CostCalculationResult(0, "", "", false);
+ }
+
+ var details = new StringBuilder("Kostenpunkt: ");
+ details.Append(cost.Name);
+ details.Append("\n\n");
+
+ var affectedTenants = AffectedTenants(billing, cost);
+ var events = CalculateEvents(billing, affectedTenants);
+
+ var pastCoveringEntries = new List();
+ var futureCoveringEntries = new List();
+ decimal totalAmount = 0;
+
+ foreach (var entry in cost.Entries)
+ {
+ totalAmount += CalculateCostEntry(billing, tenant, cost, entry, events, details, pastCoveringEntries,
+ futureCoveringEntries);
+ }
+
+ totalAmount = totalAmount.Ceil2();
+ details.Append("Betrag = ");
+ details.Append(totalAmount.CeilToString());
+ details.Append(" €\n");
+
+ var detailsLandlord = new StringBuilder(details.ToString());
+ CalculateCostInPast(billing, tenant, cost, detailsLandlord, pastCoveringEntries);
+ CalculateCostInFuture(billing, tenant, cost, detailsLandlord, futureCoveringEntries);
+ details.Append("\n");
+ detailsLandlord.Append("\n");
+
+ return new CostCalculationResult(totalAmount, details.ToString(), detailsLandlord.ToString(), true);
+ }
+
+ public static TenantCalculationResult CalculateForTenant(Billing billing, Tenant tenant)
+ {
+ var costResults = new Dictionary();
+ var details = new StringBuilder();
+ var detailsLandlord = new StringBuilder();
+
+ decimal totalAmount = 0;
+
+ foreach (var cost in billing.Costs)
+ {
+ var costResult = CalculateCost(billing, tenant, cost);
+ if (!costResult.AffectsTenant)
+ {
+ continue;
+ }
+
+ costResults.Add(cost, costResult);
+
+ totalAmount += costResult.TotalAmount;
+
+ details.Append(costResult.Details);
+ details.Append("---\n\n");
+
+ detailsLandlord.Append(costResult.DetailsForLandlord);
+ detailsLandlord.Append("---\n\n");
+ }
+
+
+ var str = $"Zwischensumme: {totalAmount.CeilToString()} €\n";
+ str += $"Bereits bezahlt: {tenant.PaidRent.CeilToString()} €\n\n";
+
+ totalAmount -= tenant.PaidRent;
+ totalAmount = totalAmount.Ceil2();
+
+ str += $"Summe: {totalAmount.CeilToString()} €";
+
+ details.Append(str);
+ detailsLandlord.Append(str);
+
+ return new TenantCalculationResult(tenant, costResults, totalAmount, details.ToString(),
+ detailsLandlord.ToString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/ucalc/Models/CostProperty.cs b/ucalc/Models/CostProperty.cs
index cbc9f9f..1ad5681 100644
--- a/ucalc/Models/CostProperty.cs
+++ b/ucalc/Models/CostProperty.cs
@@ -107,7 +107,7 @@ namespace UCalc.Models
public NotEmptyStringProperty Name { get; }
public AlwaysValidProperty Division { get; }
public AffectsAllProperty AffectsAll { get; }
- public AlwaysValidProperty IncludeUnrented { get; }
+ public AlwaysValidProperty ShiftUnrented { get; }
public AffectedFlatsProperty AffectedFlats { get; }
public CostEntriesProperty Entries { get; }
public AlwaysValidProperty DisplayInBill { get; }
@@ -119,8 +119,8 @@ namespace UCalc.Models
Division = Add(new AlwaysValidProperty(model, this, "Aufteilung", (int) cost.Division));
AffectsAll = Add(new AffectsAllProperty(model, this, "Betrifft alle", cost.AffectsAll));
AffectedFlats = Add(new AffectedFlatsProperty(model, this, cost.AffectedFlats, flatToProperty));
- IncludeUnrented =
- Add(new AlwaysValidProperty(model, this, "Unvermietete einbeziehen", cost.IncludeUnrented));
+ ShiftUnrented =
+ Add(new AlwaysValidProperty(model, this, "Unvermietete einbeziehen", cost.ShiftUnrented));
Entries = Add(new CostEntriesProperty(model, this, cost.Entries));
DisplayInBill = Add(new AlwaysValidProperty(model, this, "In Rechnung anzeigen", cost.DisplayInBill));
}
diff --git a/ucalc/Models/Model.cs b/ucalc/Models/Model.cs
index d24bdb6..1e2a370 100644
--- a/ucalc/Models/Model.cs
+++ b/ucalc/Models/Model.cs
@@ -250,7 +250,7 @@ namespace UCalc.Models
Name = cost.Name.Value,
Division = (CostDivision) cost.Division.Value,
AffectsAll = cost.AffectsAll.Value,
- IncludeUnrented = cost.IncludeUnrented.Value,
+ ShiftUnrented = cost.ShiftUnrented.Value,
AffectedFlats =
new HashSet(cost.AffectedFlats.Select(rentedFlat => flatPropertyToFlat[rentedFlat])),
Entries = cost.Entries.Select(entry => new CostEntry
diff --git a/ucalc/Models/Properties.cs b/ucalc/Models/Properties.cs
index a6c92b9..ae8a236 100644
--- a/ucalc/Models/Properties.cs
+++ b/ucalc/Models/Properties.cs
@@ -381,6 +381,8 @@ namespace UCalc.Models
{
property.ResetModified();
}
+
+ base.ResetModified();
}
public IEnumerator GetEnumerator()
diff --git a/ucalc/NewWindow.xaml b/ucalc/NewWindow.xaml
index 05ac936..9440f46 100644
--- a/ucalc/NewWindow.xaml
+++ b/ucalc/NewWindow.xaml
@@ -62,7 +62,7 @@
ItemsSource="{x:Static local:App.RecentlyOpenedList}">
-
+
diff --git a/ucalc/Pages/DetailsPage.xaml b/ucalc/Pages/DetailsPage.xaml
index 91c26cd..cc58609 100644
--- a/ucalc/Pages/DetailsPage.xaml
+++ b/ucalc/Pages/DetailsPage.xaml
@@ -3,7 +3,47 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:local="clr-namespace:UCalc.Pages"
+ xmlns:local="clr-namespace:UCalc"
+ xmlns:pages="clr-namespace:UCalc.Pages"
+ xmlns:controls="clr-namespace:UCalc.Controls"
mc:Ignorable="d">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ucalc/Pages/DetailsPage.xaml.cs b/ucalc/Pages/DetailsPage.xaml.cs
index 0202112..346dd79 100644
--- a/ucalc/Pages/DetailsPage.xaml.cs
+++ b/ucalc/Pages/DetailsPage.xaml.cs
@@ -1,19 +1,57 @@
-using System;
+using System.Collections.ObjectModel;
using System.Windows.Controls;
+using UCalc.Data;
using UCalc.Models;
namespace UCalc.Pages
{
- public partial class DetailsPage : Page
+ public partial class DetailsPage
{
+ public Model Model { get; set; }
+ private Billing _billing;
+ public ObservableCollection Tenants { get; }
+
public DetailsPage()
{
+ Tenants = new ObservableCollection();
InitializeComponent();
}
- public void Compute(Model model)
+ public void Compute()
{
- throw new NotImplementedException();
+ if (Model.Root.Errors.Count > 0)
+ {
+ TenantComboBox.IsEnabled = false;
+ CalculationTextBox.Text =
+ "Bitte beheben Sie zunächst alle Fehler bevor eine Berechnung vorgenommen werden kann.";
+ return;
+ }
+
+ _billing = Model.Dump();
+ Tenants.Clear();
+ foreach (var tenant in _billing.Tenants)
+ {
+ Tenants.Add(tenant);
+ }
+
+ TenantComboBox.IsEnabled = true;
+ TenantComboBox.SelectedIndex = -1;
+ OnSelectedTenantChanged(TenantComboBox, null);
+ }
+
+ private void OnSelectedTenantChanged(object sender, SelectionChangedEventArgs e)
+ {
+ var tenant = (Tenant) ((ComboBox) sender).SelectedItem;
+
+ if (tenant == null)
+ {
+ CalculationTextBox.Text = "Bitte wählen Sie einen Mieter aus, um die Berechnungen anzuzeigen.";
+ return;
+ }
+
+ var result = BillingCalculator.CalculateForTenant(_billing, tenant);
+
+ CalculationTextBox.Text = result.DetailsForLandlord;
}
}
}
\ No newline at end of file
diff --git a/ucalc/TenantWindow.xaml b/ucalc/TenantWindow.xaml
index 39eead2..c76ca03 100644
--- a/ucalc/TenantWindow.xaml
+++ b/ucalc/TenantWindow.xaml
@@ -17,7 +17,6 @@
-
@@ -155,9 +154,7 @@
+ SelectedDate="{Binding Path=Tenant.EntryDate.Value, RelativeSource={RelativeSource AncestorType=local:TenantWindow}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
@@ -173,9 +170,7 @@
+ SelectedDate="{Binding Path=Tenant.DepartureDate.Value, RelativeSource={RelativeSource AncestorType=local:TenantWindow}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />