From 93ef81f2f3025c3ac584bbffe0b3150d3cb0514d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Erbsh=C3=A4u=C3=9Fer?= Date: Sat, 27 Jun 2020 10:55:13 +0200 Subject: [PATCH] Implemented billing calculation. --- ucalc/BillingWindow.xaml | 1 + ucalc/BillingWindow.xaml.cs | 8 +- ucalc/Controls/Converters.cs | 22 -- ucalc/Controls/Helpers.cs | 22 +- ucalc/CostWindow.xaml | 11 +- ucalc/Data/Billing.cs | 8 +- ucalc/Data/BillingCalculator.cs | 631 ++++++++++++++++++++++++++++++++ ucalc/Models/CostProperty.cs | 6 +- ucalc/Models/Model.cs | 2 +- ucalc/Models/Properties.cs | 2 + ucalc/NewWindow.xaml | 2 +- ucalc/Pages/DetailsPage.xaml | 46 ++- ucalc/Pages/DetailsPage.xaml.cs | 46 ++- ucalc/TenantWindow.xaml | 9 +- 14 files changed, 761 insertions(+), 55 deletions(-) create mode 100644 ucalc/Data/BillingCalculator.cs 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}" />