changes3
This commit is contained in:
+112
-7
@@ -302,22 +302,105 @@ def _bar_chart(yearly: dict[int, float], title: str, width=480, height=165) -> D
|
||||
def _pie_chart(parts: list[tuple[str, float, str]], title: str, width=240, height=165) -> Drawing:
|
||||
drawing = Drawing(width, height)
|
||||
drawing.add(String(0, height - 12, title, fontName=APP_FONT_BOLD, fontSize=9, fillColor=colors.HexColor("#111827")))
|
||||
total = sum(v for _, v, _ in parts) or 1
|
||||
positive_parts = [(name, max(float(value or 0), 0.0), color) for name, value, color in parts if float(value or 0) > 0]
|
||||
total = sum(v for _, v, _ in positive_parts) or 1
|
||||
cx, cy, r = 65, 78, 45
|
||||
start = 90
|
||||
for name, value, color in parts:
|
||||
for idx, (name, value, color) in enumerate(positive_parts):
|
||||
extent = 360 * value / total
|
||||
if idx == len(positive_parts) - 1:
|
||||
extent = min(extent, 359.99)
|
||||
if extent <= 0:
|
||||
continue
|
||||
drawing.add(Wedge(cx, cy, r, start, start + extent, fillColor=colors.HexColor(color), strokeColor=colors.white, strokeWidth=0.5))
|
||||
start += extent
|
||||
ly = height - 38
|
||||
for name, value, color in parts:
|
||||
drawing.add(Rect(130, ly - 2, 7, 7, fillColor=colors.HexColor(color), strokeColor=None))
|
||||
pct = value / total * 100
|
||||
pct = max(float(value or 0), 0.0) / total * 100
|
||||
drawing.add(String(142, ly, f"{name}: {pct:.1f}%", fontName=APP_FONT, fontSize=7, fillColor=colors.HexColor("#111827")))
|
||||
ly -= 14
|
||||
return drawing
|
||||
|
||||
|
||||
def _sum_rows(rows, getter) -> float:
|
||||
return sum(float(getter(row) or 0) for row in rows)
|
||||
|
||||
|
||||
def _comparison_line_chart(actual_rows, baseline_rows, value_attr: str, title: str, actual_label: str, baseline_label: str, width=480, height=175) -> Drawing:
|
||||
drawing = Drawing(width, height)
|
||||
margin_l, margin_r, margin_b, margin_t = 42, 12, 30, 28
|
||||
plot_w = width - margin_l - margin_r
|
||||
plot_h = height - margin_t - margin_b
|
||||
x0, y0 = margin_l, margin_b
|
||||
count = max(len(actual_rows), len(baseline_rows), 1)
|
||||
actual = [float(getattr(r, value_attr)) for r in actual_rows]
|
||||
baseline = [float(getattr(r, value_attr)) for r in baseline_rows]
|
||||
max_v = max(actual + baseline + [1]) or 1
|
||||
|
||||
drawing.add(String(0, height - 12, title, fontName=APP_FONT_BOLD, fontSize=9, fillColor=colors.HexColor("#111827")))
|
||||
drawing.add(Line(x0, y0, x0, y0 + plot_h, strokeColor=colors.HexColor("#9ca3af"), strokeWidth=0.7))
|
||||
drawing.add(Line(x0, y0, x0 + plot_w, y0, strokeColor=colors.HexColor("#9ca3af"), strokeWidth=0.7))
|
||||
for i in range(1, 5):
|
||||
gy = y0 + plot_h * i / 4
|
||||
drawing.add(Line(x0, gy, x0 + plot_w, gy, strokeColor=colors.HexColor("#e5e7eb"), strokeWidth=0.4))
|
||||
label = _money(max_v * i / 4).replace(" PLN", "")
|
||||
drawing.add(String(2, gy - 3, label, fontName=APP_FONT, fontSize=6, fillColor=colors.HexColor("#6b7280")))
|
||||
|
||||
def add_series(values, color):
|
||||
if not values:
|
||||
return
|
||||
step = max(1, math.ceil(len(values) / 420))
|
||||
sampled = values[::step]
|
||||
if values[-1] != sampled[-1]:
|
||||
sampled.append(values[-1])
|
||||
sampled_count = max(len(sampled) - 1, 1)
|
||||
points = []
|
||||
for idx, value in enumerate(sampled):
|
||||
x = x0 + plot_w * idx / sampled_count
|
||||
y = y0 + plot_h * (value / max_v)
|
||||
points.extend([x, y])
|
||||
drawing.add(PolyLine(points, strokeColor=colors.HexColor(color), strokeWidth=1.4))
|
||||
|
||||
add_series(baseline, "#64748b")
|
||||
add_series(actual, "#2563eb")
|
||||
drawing.add(Rect(x0, 8, 7, 7, fillColor=colors.HexColor("#2563eb"), strokeColor=None))
|
||||
drawing.add(String(x0 + 10, 8, actual_label, fontName=APP_FONT, fontSize=6.5, fillColor=colors.HexColor("#111827")))
|
||||
drawing.add(Rect(x0 + 150, 8, 7, 7, fillColor=colors.HexColor("#64748b"), strokeColor=None))
|
||||
drawing.add(String(x0 + 160, 8, baseline_label, fontName=APP_FONT, fontSize=6.5, fillColor=colors.HexColor("#111827")))
|
||||
drawing.add(String(x0 + plot_w - 28, 8, f"{count} mies.", fontName=APP_FONT, fontSize=6, fillColor=colors.HexColor("#6b7280")))
|
||||
return drawing
|
||||
|
||||
|
||||
def _comparison_bar_chart(items: list[tuple[str, float, float]], title: str, actual_label: str, baseline_label: str, width=480, height=165) -> Drawing:
|
||||
drawing = Drawing(width, height)
|
||||
margin_l, margin_r, margin_b, margin_t = 70, 12, 34, 26
|
||||
plot_w = width - margin_l - margin_r
|
||||
plot_h = height - margin_t - margin_b
|
||||
x0, y0 = margin_l, margin_b
|
||||
max_v = max([v for _, a, b in items for v in (a, b)] + [1])
|
||||
drawing.add(String(0, height - 12, title, fontName=APP_FONT_BOLD, fontSize=9, fillColor=colors.HexColor("#111827")))
|
||||
drawing.add(Line(x0, y0, x0, y0 + plot_h, strokeColor=colors.HexColor("#9ca3af"), strokeWidth=0.7))
|
||||
drawing.add(Line(x0, y0, x0 + plot_w, y0, strokeColor=colors.HexColor("#9ca3af"), strokeWidth=0.7))
|
||||
for i in range(1, 5):
|
||||
gy = y0 + plot_h * i / 4
|
||||
drawing.add(Line(x0, gy, x0 + plot_w, gy, strokeColor=colors.HexColor("#e5e7eb"), strokeWidth=0.4))
|
||||
group_w = plot_w / max(len(items), 1)
|
||||
bar_w = min(28, max(9, group_w / 3.2))
|
||||
for idx, (label, actual, baseline) in enumerate(items):
|
||||
center = x0 + group_w * idx + group_w / 2
|
||||
ah = plot_h * actual / max_v
|
||||
bh = plot_h * baseline / max_v
|
||||
drawing.add(Rect(center - bar_w - 1, y0, bar_w, ah, fillColor=colors.HexColor("#2563eb"), strokeColor=None))
|
||||
drawing.add(Rect(center + 1, y0, bar_w, bh, fillColor=colors.HexColor("#64748b"), strokeColor=None))
|
||||
drawing.add(String(center - 28, 12, label, fontName=APP_FONT, fontSize=6, fillColor=colors.HexColor("#6b7280")))
|
||||
drawing.add(Rect(x0, 2, 7, 7, fillColor=colors.HexColor("#2563eb"), strokeColor=None))
|
||||
drawing.add(String(x0 + 10, 2, actual_label, fontName=APP_FONT, fontSize=6.5, fillColor=colors.HexColor("#111827")))
|
||||
drawing.add(Rect(x0 + 150, 2, 7, 7, fillColor=colors.HexColor("#64748b"), strokeColor=None))
|
||||
drawing.add(String(x0 + 160, 2, baseline_label, fontName=APP_FONT, fontSize=6.5, fillColor=colors.HexColor("#111827")))
|
||||
return drawing
|
||||
|
||||
|
||||
@app.post("/api/export/pdf")
|
||||
def export_pdf(req: SimulationRequest):
|
||||
result = simulate(req)
|
||||
@@ -366,12 +449,18 @@ def export_pdf(req: SimulationRequest):
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
|
||||
s = result.summary
|
||||
simulated_costs = _sum_rows(result.schedule, lambda r: r.interest_part + r.overpayment_fee)
|
||||
nominal_costs = _sum_rows(result.baseline_schedule, lambda r: r.interest_part)
|
||||
simulated_payments = _sum_rows(result.schedule, lambda r: r.payment + r.overpayment + r.overpayment_fee)
|
||||
nominal_payments = _sum_rows(result.baseline_schedule, lambda r: r.payment)
|
||||
summary = [
|
||||
["Liczba rat", s.months, "Bazowo", s.baseline_months],
|
||||
["Suma zaplacona", _money(s.total_paid), "Suma odsetek", _money(s.total_interest)],
|
||||
["Oszczednosc na odsetkach", _money(s.interest_saved), "Skrocenie okresu", f"{s.months_saved} mies."],
|
||||
["Suma nadplat", _money(s.total_overpayment), "Prowizje nadplat", _money(s.total_overpayment_fees)],
|
||||
["Srednia / maks. rata", f"{_money(s.average_payment)} / {_money(s.max_payment)}", "Data splaty", s.payoff_date or "-"],
|
||||
["Koszty nominalne", _money(nominal_costs), "Koszty po symulacji", _money(simulated_costs)],
|
||||
["Platnosci nominalne", _money(nominal_payments), "Platnosci po symulacji", _money(simulated_payments)],
|
||||
]
|
||||
story.append(Paragraph("Podsumowanie", styles["Heading2"]))
|
||||
story.append(Table(summary, colWidths=[4.3 * cm, 4.25 * cm, 3.7 * cm, 4.35 * cm], style=TableStyle([
|
||||
@@ -383,19 +472,35 @@ def export_pdf(req: SimulationRequest):
|
||||
("FONTSIZE", (0, 0), (-1, -1), 7.0),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
])))
|
||||
story.append(Spacer(1, 0.2 * cm))
|
||||
result_description = (
|
||||
f"Opis wyniku: nominalne koszty kredytu wynosza {_money(nominal_costs)}, "
|
||||
f"a po symulowanych modyfikacjach {_money(simulated_costs)}. "
|
||||
f"Roznica kosztow to {_money(nominal_costs - simulated_costs)}. "
|
||||
f"Laczne platnosci nominalne wynosza {_money(nominal_payments)}, "
|
||||
f"a po symulacji {_money(simulated_payments)}. "
|
||||
f"Kredyt konczy sie po {s.months} miesiacach zamiast {s.baseline_months}."
|
||||
)
|
||||
story.append(Paragraph(result_description, styles["Small"]))
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
|
||||
story.append(Paragraph("Wykresy", styles["Heading2"]))
|
||||
yearly = {}
|
||||
for row in result.schedule:
|
||||
year = math.ceil(row.month / 12)
|
||||
yearly[year] = yearly.get(year, 0.0) + row.interest_part
|
||||
story.append(_line_chart(result.schedule, "remaining", "Saldo kredytu w czasie"))
|
||||
yearly[year] = yearly.get(year, 0.0) + row.interest_part + row.overpayment_fee
|
||||
story.append(_comparison_line_chart(result.schedule, result.baseline_schedule, "remaining", "Saldo: nominalnie vs po modyfikacjach", "po modyfikacjach", "nominalnie"))
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
story.append(_line_chart(result.schedule, "payment", "Rata w czasie"))
|
||||
story.append(_comparison_line_chart(result.schedule, result.baseline_schedule, "payment", "Rata: nominalnie vs po modyfikacjach", "po modyfikacjach", "nominalnie"))
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
story.append(_comparison_bar_chart([
|
||||
("Koszty", simulated_costs, nominal_costs),
|
||||
("Platnosci", simulated_payments, nominal_payments),
|
||||
("Odsetki", s.total_interest, s.baseline_interest),
|
||||
], "Porownanie oplat nominalnych i po symulacji", "po symulacji", "nominalnie"))
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
story.append(Table([[
|
||||
_bar_chart(yearly, "Odsetki rocznie", width=285, height=150),
|
||||
_bar_chart(yearly, "Koszty rocznie po symulacji", width=285, height=150),
|
||||
_pie_chart([
|
||||
("Kapital", req.principal, "#2563eb"),
|
||||
("Odsetki", s.total_interest, "#64748b"),
|
||||
|
||||
Reference in New Issue
Block a user