wbreport 1.43.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbreport might be problematic. Click here for more details.
- wbreport/__init__.py +1 -0
- wbreport/admin.py +87 -0
- wbreport/apps.py +6 -0
- wbreport/defaults/__init__.py +0 -0
- wbreport/defaults/factsheets/__init__.py +0 -0
- wbreport/defaults/factsheets/base.py +990 -0
- wbreport/defaults/factsheets/menu.py +93 -0
- wbreport/defaults/factsheets/mixins.py +35 -0
- wbreport/defaults/factsheets/multitheme.py +947 -0
- wbreport/dynamic_preferences_registry.py +15 -0
- wbreport/factories/__init__.py +8 -0
- wbreport/factories/data_classes.py +48 -0
- wbreport/factories/reports.py +79 -0
- wbreport/filters.py +37 -0
- wbreport/fixtures/wbreport.yaml +1 -0
- wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
- wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
- wbreport/migrations/0009_alter_report_color_palette.py +27 -0
- wbreport/migrations/0010_auto_20240103_0947.py +43 -0
- wbreport/migrations/0011_auto_20240207_1629.py +35 -0
- wbreport/migrations/0012_reportversion_lock.py +17 -0
- wbreport/migrations/0013_alter_reportversion_context.py +18 -0
- wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
- wbreport/migrations/__init__.py +0 -0
- wbreport/mixins.py +183 -0
- wbreport/models.py +781 -0
- wbreport/pdf/__init__.py +0 -0
- wbreport/pdf/charts/__init__.py +0 -0
- wbreport/pdf/charts/legend.py +15 -0
- wbreport/pdf/charts/pie.py +169 -0
- wbreport/pdf/charts/timeseries.py +77 -0
- wbreport/pdf/flowables/risk.py +88 -0
- wbreport/pdf/flowables/textboxes.py +143 -0
- wbreport/pdf/flowables/themes.py +179 -0
- wbreport/pdf/sandbox/__init__.py +0 -0
- wbreport/pdf/sandbox/run.py +17 -0
- wbreport/pdf/sandbox/templates/__init__.py +0 -0
- wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
- wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
- wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
- wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
- wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
- wbreport/pdf/tables/__init__.py +0 -0
- wbreport/pdf/tables/aggregated_tables.py +156 -0
- wbreport/pdf/tables/data_tables.py +75 -0
- wbreport/serializers.py +191 -0
- wbreport/tasks.py +60 -0
- wbreport/templates/__init__.py +0 -0
- wbreport/templatetags/__init__.py +0 -0
- wbreport/templatetags/portfolio_tags.py +35 -0
- wbreport/tests/__init__.py +0 -0
- wbreport/tests/conftest.py +24 -0
- wbreport/tests/test_models.py +253 -0
- wbreport/tests/test_tasks.py +17 -0
- wbreport/tests/test_viewsets.py +0 -0
- wbreport/tests/tests.py +12 -0
- wbreport/urls.py +29 -0
- wbreport/urls_public.py +10 -0
- wbreport/viewsets/__init__.py +10 -0
- wbreport/viewsets/configs/__init__.py +18 -0
- wbreport/viewsets/configs/buttons.py +193 -0
- wbreport/viewsets/configs/displays.py +116 -0
- wbreport/viewsets/configs/endpoints.py +23 -0
- wbreport/viewsets/configs/menus.py +8 -0
- wbreport/viewsets/configs/titles.py +30 -0
- wbreport/viewsets/viewsets.py +330 -0
- wbreport-1.43.1.dist-info/METADATA +7 -0
- wbreport-1.43.1.dist-info/RECORD +70 -0
- wbreport-1.43.1.dist-info/WHEEL +5 -0
- wbreport-1.43.1.dist-info/licenses/LICENSE +4 -0
wbreport/pdf/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from reportlab.graphics.charts.legends import Legend
|
|
2
|
+
from reportlab.graphics.shapes import Circle
|
|
3
|
+
from reportlab.lib.colors import transparent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CustomLegend(Legend):
|
|
7
|
+
def _defaultSwatch(self, x, thisy, dx, dy, fillColor, strokeWidth, strokeColor):
|
|
8
|
+
return Circle(
|
|
9
|
+
x,
|
|
10
|
+
thisy + dx / 2,
|
|
11
|
+
dx / 2,
|
|
12
|
+
fillColor=fillColor,
|
|
13
|
+
strokeColor=transparent,
|
|
14
|
+
strokeWidth=strokeWidth,
|
|
15
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from reportlab.graphics.charts.piecharts import Pie
|
|
2
|
+
from reportlab.graphics.shapes import Drawing
|
|
3
|
+
from reportlab.lib import colors
|
|
4
|
+
from reportlab.lib.units import cm
|
|
5
|
+
from wbreport.pdf.charts.legend import CustomLegend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_data_and_labels_from_df(df, color_palette, percent=True):
|
|
9
|
+
data = list()
|
|
10
|
+
colornamepairs = list()
|
|
11
|
+
|
|
12
|
+
for index, row in enumerate(df.itertuples()):
|
|
13
|
+
data.append(float(row.weighting))
|
|
14
|
+
|
|
15
|
+
label = f"{row[2]*100:.1f}%" if percent else f"{row[2]:.1f}"
|
|
16
|
+
colornamepairs.append((colors.HexColor(color_palette[index]), f"{row[1]} {label}"))
|
|
17
|
+
|
|
18
|
+
return data, colornamepairs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_legend_height(df, r2_legend=0.3 * cm, legend_padding=1, legend_max_cols=7):
|
|
22
|
+
return min(len(df), legend_max_cols) * (r2_legend + legend_padding)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_pie_chart_vertical_height(
|
|
26
|
+
df,
|
|
27
|
+
r2=2.5 * cm,
|
|
28
|
+
padding=0.45 * cm,
|
|
29
|
+
r2_legend=0.3 * cm,
|
|
30
|
+
legend_padding=1,
|
|
31
|
+
legend_max_cols=7,
|
|
32
|
+
):
|
|
33
|
+
return 3 * padding + r2 + get_legend_height(df, r2_legend, legend_padding, legend_max_cols)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_pie_chart_vertical(
|
|
37
|
+
df,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
color_palette,
|
|
41
|
+
r2=2.5 * cm,
|
|
42
|
+
padding=0.45 * cm,
|
|
43
|
+
r2_legend=0.3 * cm,
|
|
44
|
+
legend_padding=1,
|
|
45
|
+
legend_max_cols=7,
|
|
46
|
+
):
|
|
47
|
+
drawing = Drawing(width, height)
|
|
48
|
+
pie = Pie()
|
|
49
|
+
|
|
50
|
+
pie.width = r2
|
|
51
|
+
pie.height = r2
|
|
52
|
+
|
|
53
|
+
pie.x = (width - r2) / 2
|
|
54
|
+
pie.y = height - r2 - padding
|
|
55
|
+
df = df.sort_values(by=["weighting"], ascending=False)
|
|
56
|
+
data, colornamepairs = get_data_and_labels_from_df(df, color_palette)
|
|
57
|
+
|
|
58
|
+
for i, color in enumerate(colornamepairs):
|
|
59
|
+
pie.slices[i].fillColor = color[0]
|
|
60
|
+
pie.slices[i].strokeColor = colors.transparent
|
|
61
|
+
pie.slices[i].strokeWidth = 0 # Border width for wedge
|
|
62
|
+
pie.slices.strokeWidth = 0 # Width of the border around the pie chart.
|
|
63
|
+
pie.data = data
|
|
64
|
+
|
|
65
|
+
drawing.add(pie)
|
|
66
|
+
|
|
67
|
+
legend = CustomLegend()
|
|
68
|
+
|
|
69
|
+
legend.alignment = "right"
|
|
70
|
+
legend.boxAnchor = "nw"
|
|
71
|
+
legend.x = 0
|
|
72
|
+
legend.y = height - r2 - 2 * padding
|
|
73
|
+
legend.dx = legend.dy = r2_legend
|
|
74
|
+
legend.strokeWidth = -1
|
|
75
|
+
legend.columnMaximum = legend_max_cols
|
|
76
|
+
legend.colorNamePairs = colornamepairs
|
|
77
|
+
legend.fontName = "customfont"
|
|
78
|
+
legend.fontSize = 6
|
|
79
|
+
|
|
80
|
+
legend.deltax = 0 # Here
|
|
81
|
+
legend.deltay = legend_padding
|
|
82
|
+
legend.swdx = 12
|
|
83
|
+
legend.swdy = 0
|
|
84
|
+
|
|
85
|
+
legend.dxTextSpace = r2_legend
|
|
86
|
+
|
|
87
|
+
legend.variColumn = True
|
|
88
|
+
|
|
89
|
+
drawing.add(legend)
|
|
90
|
+
return drawing
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_pie_chart_horizontal_height(
|
|
94
|
+
df,
|
|
95
|
+
r2=2.5 * cm,
|
|
96
|
+
padding=0.45 * cm,
|
|
97
|
+
r2_legend=0.3 * cm,
|
|
98
|
+
legend_padding=1,
|
|
99
|
+
legend_max_cols=7,
|
|
100
|
+
):
|
|
101
|
+
return max(
|
|
102
|
+
2 * padding + r2,
|
|
103
|
+
2 * padding + get_legend_height(df, r2_legend, legend_padding, legend_max_cols),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_pie_chart_horizontal(
|
|
108
|
+
df,
|
|
109
|
+
width,
|
|
110
|
+
height,
|
|
111
|
+
color_palette,
|
|
112
|
+
col_width,
|
|
113
|
+
r2=2.5 * cm,
|
|
114
|
+
padding=0.45 * cm,
|
|
115
|
+
r2_legend=0.3 * cm,
|
|
116
|
+
legend_padding=1,
|
|
117
|
+
legend_max_cols=7,
|
|
118
|
+
legend_x=None,
|
|
119
|
+
):
|
|
120
|
+
drawing = Drawing(width, height)
|
|
121
|
+
pie = Pie()
|
|
122
|
+
|
|
123
|
+
pie.width = r2
|
|
124
|
+
pie.height = r2
|
|
125
|
+
|
|
126
|
+
pie.x = (col_width - r2) / 2
|
|
127
|
+
pie.y = height - r2 - padding
|
|
128
|
+
df = df.sort_values(by=["weighting"], ascending=False)
|
|
129
|
+
data, colornamepairs = get_data_and_labels_from_df(df, color_palette)
|
|
130
|
+
|
|
131
|
+
for i, color in enumerate(colornamepairs):
|
|
132
|
+
pie.slices[i].fillColor = color[0]
|
|
133
|
+
pie.slices[i].strokeColor = colors.transparent
|
|
134
|
+
pie.slices[i].strokeWidth = 0 # Border width for wedge.
|
|
135
|
+
pie.slices.strokeWidth = 0 # Width of the border around the pie chart.
|
|
136
|
+
pie.data = data
|
|
137
|
+
|
|
138
|
+
drawing.add(pie)
|
|
139
|
+
|
|
140
|
+
legend = CustomLegend()
|
|
141
|
+
|
|
142
|
+
# legend.x = 4.82 * cm
|
|
143
|
+
# legend.y += 0.78 * cm
|
|
144
|
+
|
|
145
|
+
legend_height = get_legend_height(df, r2_legend, legend_padding, legend_max_cols)
|
|
146
|
+
|
|
147
|
+
legend.alignment = "right"
|
|
148
|
+
legend.boxAnchor = "nw"
|
|
149
|
+
|
|
150
|
+
if legend_x:
|
|
151
|
+
legend.x = legend_x
|
|
152
|
+
else:
|
|
153
|
+
legend.x = width - col_width
|
|
154
|
+
|
|
155
|
+
legend.y = (height + legend_height) / 2
|
|
156
|
+
legend.dx = legend.dy = r2_legend
|
|
157
|
+
legend.strokeWidth = -1
|
|
158
|
+
legend.columnMaximum = legend_max_cols
|
|
159
|
+
legend.colorNamePairs = colornamepairs
|
|
160
|
+
legend.fontName = "customfont"
|
|
161
|
+
legend.fontSize = 6
|
|
162
|
+
|
|
163
|
+
legend.deltax = 0
|
|
164
|
+
legend.deltay = legend_padding
|
|
165
|
+
legend.swdx = 12
|
|
166
|
+
legend.swdy = 0
|
|
167
|
+
|
|
168
|
+
drawing.add(legend)
|
|
169
|
+
return drawing
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from reportlab.graphics.charts.axes import LogYValueAxis, NormalDateXValueAxis
|
|
4
|
+
from reportlab.graphics.charts.lineplots import LinePlot, SimpleTimeSeriesPlot
|
|
5
|
+
from reportlab.graphics.shapes import Drawing
|
|
6
|
+
from reportlab.lib import colors
|
|
7
|
+
from reportlab.lib.units import cm
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Scale(Enum):
|
|
11
|
+
ARITHMETIC = 1
|
|
12
|
+
LOGARITHMIC = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogScaleTimeSeriesPlot(LinePlot):
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
class CustomYAxis(LogYValueAxis):
|
|
20
|
+
def _calcTickPositions(self):
|
|
21
|
+
return self._calcStepAndTickPositions()[1]
|
|
22
|
+
|
|
23
|
+
self.xValueAxis = NormalDateXValueAxis()
|
|
24
|
+
self.yValueAxis = CustomYAxis()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_timeseries_chart(
|
|
28
|
+
data, width, height, color, fill_color, grid_color, scale, x_label_format="{mm}/{yy}", **chart_attributes
|
|
29
|
+
):
|
|
30
|
+
width -= 0.65 * cm
|
|
31
|
+
chart_map = {
|
|
32
|
+
Scale.ARITHMETIC.value: SimpleTimeSeriesPlot,
|
|
33
|
+
Scale.LOGARITHMIC.value: LogScaleTimeSeriesPlot,
|
|
34
|
+
}
|
|
35
|
+
data = [list(filter(lambda x: x[1] > 0, data[0]))]
|
|
36
|
+
max_x_ticks = 20
|
|
37
|
+
|
|
38
|
+
# ensure that if the data count is too low (e.g. less than a month range), we don't overcrowds the X axis
|
|
39
|
+
if len(data[0]) < max_x_ticks:
|
|
40
|
+
x_label_format = "{dd}/{mm}/{yy}"
|
|
41
|
+
max_x_ticks = 10
|
|
42
|
+
|
|
43
|
+
drawing = Drawing(width, height)
|
|
44
|
+
|
|
45
|
+
chart = chart_map[scale]()
|
|
46
|
+
chart.width = width
|
|
47
|
+
chart.height = height
|
|
48
|
+
|
|
49
|
+
for key, value in chart_attributes.items():
|
|
50
|
+
setattr(chart, key, value)
|
|
51
|
+
|
|
52
|
+
chart.data = data
|
|
53
|
+
|
|
54
|
+
chart.lines[0].strokeWidth = 0.5
|
|
55
|
+
chart.lines[0].strokeColor = color
|
|
56
|
+
chart.lines[0].fillColor = fill_color
|
|
57
|
+
chart.lines[0].inFill = True
|
|
58
|
+
|
|
59
|
+
chart.yValueAxis.strokeWidth = -1
|
|
60
|
+
chart.yValueAxis.strokeColor = colors.white
|
|
61
|
+
chart.yValueAxis.labels.fontSize = 7
|
|
62
|
+
chart.yValueAxis.labels.fontName = "customfont"
|
|
63
|
+
chart.yValueAxis.labels.dx = width + 0.55 * cm
|
|
64
|
+
chart.yValueAxis.maximumTicks = 100
|
|
65
|
+
chart.yValueAxis.labelTextFormat = "%d"
|
|
66
|
+
|
|
67
|
+
chart.xValueAxis.strokeWidth = 0
|
|
68
|
+
chart.xValueAxis.labels.fontSize = 7
|
|
69
|
+
chart.xValueAxis.labels.fontName = "customfont"
|
|
70
|
+
chart.xValueAxis.maximumTicks = max_x_ticks
|
|
71
|
+
chart.xValueAxis.visibleGrid = 1
|
|
72
|
+
chart.xValueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
|
|
73
|
+
chart.xValueAxis.gridStrokeColor = grid_color
|
|
74
|
+
chart.xValueAxis.xLabelFormat = x_label_format
|
|
75
|
+
|
|
76
|
+
drawing.add(chart)
|
|
77
|
+
return drawing
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from reportlab.lib.colors import HexColor, black, white
|
|
2
|
+
from reportlab.lib.units import cm
|
|
3
|
+
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
4
|
+
from reportlab.platypus import Flowable, Paragraph
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RiskScale(Flowable):
|
|
8
|
+
def __init__(self, risk, para_style, text=None):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.risk = risk
|
|
11
|
+
self.risk_text = "The actual risk can vary significantly if you cash in at an early stage and you may get back less. You may not be able to sell your product easily or you may have to sell at a price that significantly impacts on how much you get back."
|
|
12
|
+
self.additional_text = text
|
|
13
|
+
self.para_style = para_style
|
|
14
|
+
self.height = 3.564 * cm
|
|
15
|
+
if text:
|
|
16
|
+
self.height += 18
|
|
17
|
+
|
|
18
|
+
def draw(self):
|
|
19
|
+
width = 0.4 * cm
|
|
20
|
+
gap = 1.177 * cm
|
|
21
|
+
|
|
22
|
+
y = self.height - 0.868 * cm
|
|
23
|
+
x_offset = 0.883 * cm
|
|
24
|
+
self.canv.setFillColor(HexColor(0x9EA3AC))
|
|
25
|
+
for x in range(7):
|
|
26
|
+
_x = x * gap + x_offset
|
|
27
|
+
if x == self.risk - 1:
|
|
28
|
+
self.canv.setFillColor(HexColor(0x3C4859))
|
|
29
|
+
self.canv.circle(_x, y, width, fill=True, stroke=False)
|
|
30
|
+
self.canv.setFillColor(HexColor(0x9EA3AC))
|
|
31
|
+
else:
|
|
32
|
+
self.canv.circle(_x, y, width, fill=True, stroke=False)
|
|
33
|
+
|
|
34
|
+
self.canv.setFillColor(white)
|
|
35
|
+
self.canv.setFont("customfont-bd", 11)
|
|
36
|
+
self.canv.drawCentredString(_x, y - 4, str(x + 1))
|
|
37
|
+
self.canv.setFillColor(HexColor(0x9EA3AC))
|
|
38
|
+
|
|
39
|
+
self.canv.setFillColor(HexColor(0x6D7683))
|
|
40
|
+
self.canv.setStrokeColor(HexColor(0x6D7683))
|
|
41
|
+
|
|
42
|
+
arrow_offset = 0.2 * cm
|
|
43
|
+
|
|
44
|
+
p = self.canv.beginPath()
|
|
45
|
+
origin = (x_offset - arrow_offset, y - 0.868 * cm)
|
|
46
|
+
p.moveTo(*origin)
|
|
47
|
+
p.lineTo(origin[0] + 0.059 * cm, origin[1] + 0.08 * cm)
|
|
48
|
+
p.lineTo(origin[0] - 0.165 * cm, origin[1])
|
|
49
|
+
p.lineTo(origin[0] + 0.059 * cm, origin[1] - 0.08 * cm)
|
|
50
|
+
self.canv.drawPath(p, fill=True, stroke=False)
|
|
51
|
+
|
|
52
|
+
p = self.canv.beginPath()
|
|
53
|
+
origin = (6 * gap + x_offset + arrow_offset, y - 0.868 * cm)
|
|
54
|
+
p.moveTo(origin[0], origin[1])
|
|
55
|
+
p.lineTo(origin[0] - 0.059 * cm, origin[1] + 0.08 * cm)
|
|
56
|
+
p.lineTo(origin[0] + 0.165 * cm, origin[1])
|
|
57
|
+
p.lineTo(origin[0] - 0.059 * cm, origin[1] - 0.08 * cm)
|
|
58
|
+
self.canv.drawPath(p, fill=True, stroke=False)
|
|
59
|
+
|
|
60
|
+
self.canv.setLineWidth(0.02 * cm)
|
|
61
|
+
p = self.canv.beginPath()
|
|
62
|
+
self.canv.line(
|
|
63
|
+
x_offset - arrow_offset,
|
|
64
|
+
y - 0.868 * cm,
|
|
65
|
+
6 * gap + x_offset + arrow_offset,
|
|
66
|
+
y - 0.868 * cm,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.canv.setFont("customfont", 6)
|
|
70
|
+
self.canv.setFillColor(black)
|
|
71
|
+
self.canv.drawString(x_offset - arrow_offset - 0.165 * cm, y - 1.2 * cm, "LOWER RISK")
|
|
72
|
+
|
|
73
|
+
text_width = stringWidth("HIGHER RISK", "customfont", 6)
|
|
74
|
+
self.canv.drawString(
|
|
75
|
+
6 * gap + x_offset + arrow_offset + 0.165 * cm - text_width,
|
|
76
|
+
y - 1.2 * cm,
|
|
77
|
+
"HIGHER RISK",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
para = Paragraph(self.risk_text, style=self.para_style)
|
|
81
|
+
para.wrapOn(self.canv, 250, 8.954 * cm)
|
|
82
|
+
para.drawOn(self.canv, 0, y - 2.5 * cm)
|
|
83
|
+
|
|
84
|
+
if self.additional_text:
|
|
85
|
+
para = Paragraph(f"<i>* {self.additional_text}</i>", style=self.para_style)
|
|
86
|
+
|
|
87
|
+
para.wrapOn(self.canv, 250, 8.954 * cm)
|
|
88
|
+
para.drawOn(self.canv, 0, y - 3.2 * cm)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from reportlab.graphics import renderPDF
|
|
2
|
+
from reportlab.lib.colors import black
|
|
3
|
+
from reportlab.lib.units import cm
|
|
4
|
+
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
5
|
+
from reportlab.platypus import Flowable, Image, Paragraph, Table, TableStyle
|
|
6
|
+
from svglib.svglib import svg2rlg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TextBox(Flowable):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
width,
|
|
13
|
+
height,
|
|
14
|
+
text,
|
|
15
|
+
text_style,
|
|
16
|
+
box_color,
|
|
17
|
+
grid_width=0.30 * cm,
|
|
18
|
+
offset=None,
|
|
19
|
+
debug=False,
|
|
20
|
+
):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.width = width
|
|
23
|
+
self.height = height
|
|
24
|
+
self.offset = offset or 0
|
|
25
|
+
self.grid_width = grid_width
|
|
26
|
+
|
|
27
|
+
self.text_table = Table([[Paragraph(text, style=text_style)]], rowHeights=[height - grid_width])
|
|
28
|
+
table_styles = [
|
|
29
|
+
("BACKGROUND", (0, 0), (0, 0), box_color),
|
|
30
|
+
("GRID", (0, 0), (0, 0), grid_width, box_color),
|
|
31
|
+
("VALIGN", (0, 0), (0, 0), "MIDDLE"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if debug:
|
|
35
|
+
table_styles.extend(
|
|
36
|
+
[
|
|
37
|
+
("BOX", (0, 0), (-1, -1), 0.25, black),
|
|
38
|
+
("INNERGRID", (0, 0), (-1, -1), 0.25, black),
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.text_table.setStyle(TableStyle(table_styles))
|
|
43
|
+
|
|
44
|
+
def draw(self):
|
|
45
|
+
self.text_table.wrapOn(self.canv, self.width - self.grid_width - self.offset, self.height)
|
|
46
|
+
self.text_table.drawOn(self.canv, (self.offset or 0) + (self.grid_width / 2), (self.grid_width / 2))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TextBoxWithImage(TextBox):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
width,
|
|
53
|
+
height,
|
|
54
|
+
img,
|
|
55
|
+
img_x,
|
|
56
|
+
img_y,
|
|
57
|
+
img_width,
|
|
58
|
+
img_height,
|
|
59
|
+
text,
|
|
60
|
+
text_style,
|
|
61
|
+
box_color,
|
|
62
|
+
grid_width=0.28 * cm,
|
|
63
|
+
svg=False,
|
|
64
|
+
offset=None,
|
|
65
|
+
debug=False,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(width, height, text, text_style, box_color, grid_width, offset, debug)
|
|
68
|
+
self.img = img
|
|
69
|
+
self.img_x = img_x
|
|
70
|
+
self.img_y = img_y
|
|
71
|
+
self.img_width = img_width
|
|
72
|
+
self.img_height = img_height
|
|
73
|
+
self.svg = svg
|
|
74
|
+
|
|
75
|
+
def draw(self):
|
|
76
|
+
super().draw()
|
|
77
|
+
|
|
78
|
+
if self.svg:
|
|
79
|
+
raise NotImplementedError("SVG is not yet implemented.")
|
|
80
|
+
else:
|
|
81
|
+
img = Image(self.img)
|
|
82
|
+
img.drawWidth = self.img_width
|
|
83
|
+
img.drawHeight = self.img_height
|
|
84
|
+
img.drawOn(self.canv, self.img_x, self.img_y)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TextWithIcon(Flowable):
|
|
88
|
+
def __init__(self, width, height, text, font, font_size, icon=None):
|
|
89
|
+
self.width = width
|
|
90
|
+
self.height = height
|
|
91
|
+
self.text = text
|
|
92
|
+
self.font = font
|
|
93
|
+
self.font_size = font_size
|
|
94
|
+
self.icon = icon
|
|
95
|
+
self.svg = None
|
|
96
|
+
if icon:
|
|
97
|
+
self.svg = self.icon.name.split(".")[-1] in ["svg", "SVG"]
|
|
98
|
+
|
|
99
|
+
def draw(self):
|
|
100
|
+
# Get text width and set font
|
|
101
|
+
text_width = stringWidth(self.text, self.font, self.font_size)
|
|
102
|
+
self.canv.setFont(self.font, self.font_size, leading=self.font_size)
|
|
103
|
+
|
|
104
|
+
# Get icon
|
|
105
|
+
icon = None
|
|
106
|
+
if self.svg:
|
|
107
|
+
icon = svg2rlg(self.icon)
|
|
108
|
+
elif self.icon and self.icon.name:
|
|
109
|
+
icon = Image(self.icon)
|
|
110
|
+
|
|
111
|
+
tx = self.width / (4 if icon else 2) - text_width / 2
|
|
112
|
+
ty = self.height / 2 - self.font_size / 2
|
|
113
|
+
self.canv.drawString(tx, ty, self.text)
|
|
114
|
+
|
|
115
|
+
if icon:
|
|
116
|
+
if self.svg:
|
|
117
|
+
icon_width = icon.width
|
|
118
|
+
icon_height = icon.height
|
|
119
|
+
else:
|
|
120
|
+
icon_width = icon.imageWidth
|
|
121
|
+
icon_height = icon.imageHeight
|
|
122
|
+
|
|
123
|
+
drawing_height = self.height / 2
|
|
124
|
+
scale = drawing_height / icon_height
|
|
125
|
+
drawing_width = icon_width * scale
|
|
126
|
+
|
|
127
|
+
allowed_width = (self.width / 2) - 0.2 * cm
|
|
128
|
+
|
|
129
|
+
if drawing_width > allowed_width:
|
|
130
|
+
drawing_width = allowed_width
|
|
131
|
+
scale = drawing_width / icon_width
|
|
132
|
+
drawing_height = icon_height * scale
|
|
133
|
+
|
|
134
|
+
dx = (self.width / 4) * 3 - (icon_width * scale) / 2
|
|
135
|
+
dy = (self.height - drawing_height) / 2
|
|
136
|
+
|
|
137
|
+
if self.svg:
|
|
138
|
+
icon.scale(scale, scale)
|
|
139
|
+
renderPDF.draw(icon, self.canv, dx, dy)
|
|
140
|
+
else:
|
|
141
|
+
icon.drawWidth = drawing_width
|
|
142
|
+
icon.drawHeight = drawing_height
|
|
143
|
+
icon.drawOn(self.canv, dx, dy)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from reportlab.graphics.charts.barcharts import HorizontalBarChart
|
|
2
|
+
from reportlab.graphics.charts.piecharts import Pie
|
|
3
|
+
from reportlab.graphics.shapes import Drawing
|
|
4
|
+
from reportlab.lib.colors import HexColor, transparent
|
|
5
|
+
from reportlab.lib.formatters import DecimalFormatter
|
|
6
|
+
from reportlab.lib.units import cm
|
|
7
|
+
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
8
|
+
from reportlab.platypus import Flowable
|
|
9
|
+
from wbreport.pdf.charts.legend import CustomLegend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThemeBreakdown(Flowable):
|
|
13
|
+
def __init__(self, df, width, grid_color):
|
|
14
|
+
# self.df = self.df[self.df["allocation_end"] > 0]
|
|
15
|
+
self.df = df[df["allocation_end"] != 0]
|
|
16
|
+
self.pie_padding = 0.45 * cm
|
|
17
|
+
self.pie_diameter = 2.5 * cm
|
|
18
|
+
self.pie_legend_diameter = 0.3 * cm
|
|
19
|
+
self.pie_legend_padding = 1
|
|
20
|
+
self.pie_legend_max_cols = len(self.df)
|
|
21
|
+
|
|
22
|
+
self.bar_width = 0.24 * cm
|
|
23
|
+
self.bar_padding = 0.3 * cm
|
|
24
|
+
self.bar_max_label_width = 3.35 * cm
|
|
25
|
+
self.bar_label_offset = 5
|
|
26
|
+
|
|
27
|
+
self.font_size = 6
|
|
28
|
+
self.font_name = "customfont"
|
|
29
|
+
|
|
30
|
+
self.width = width
|
|
31
|
+
self.height = self.get_height()
|
|
32
|
+
|
|
33
|
+
self.grid_color = grid_color
|
|
34
|
+
|
|
35
|
+
def get_pie_chart_legend_height(self):
|
|
36
|
+
return min(len(self.df), self.pie_legend_max_cols) * (self.pie_legend_diameter + self.pie_legend_padding)
|
|
37
|
+
|
|
38
|
+
def get_pie_chart_height(self):
|
|
39
|
+
return max(
|
|
40
|
+
2 * self.pie_padding + self.pie_diameter,
|
|
41
|
+
2 * self.pie_padding + self.get_pie_chart_legend_height(),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def get_bar_chart_height(self):
|
|
45
|
+
num_bars = len(self.df) * 2
|
|
46
|
+
|
|
47
|
+
return num_bars * self.bar_width + num_bars / 2 * self.bar_padding
|
|
48
|
+
|
|
49
|
+
def get_height(self):
|
|
50
|
+
pie_chart_height = self.get_pie_chart_height()
|
|
51
|
+
bar_chart_height = self.get_bar_chart_height()
|
|
52
|
+
|
|
53
|
+
return pie_chart_height + bar_chart_height + 40
|
|
54
|
+
|
|
55
|
+
def get_pie_chart_with_legend(self):
|
|
56
|
+
pie = Pie()
|
|
57
|
+
|
|
58
|
+
pie.x = (self.width / 2 - self.pie_diameter) / 2
|
|
59
|
+
pie.y = self.height - self.pie_diameter - self.pie_padding
|
|
60
|
+
pie.width = self.pie_diameter
|
|
61
|
+
pie.height = self.pie_diameter
|
|
62
|
+
|
|
63
|
+
pie_data = list()
|
|
64
|
+
pie_colornamepairs = list()
|
|
65
|
+
max_width_colornamepair = 0
|
|
66
|
+
|
|
67
|
+
self.df = self.df.sort_values(by=["allocation_end"], ascending=False).rename(
|
|
68
|
+
columns={"underlying_instrument__title_repr": "underlying_instrument__name_repr"}
|
|
69
|
+
)
|
|
70
|
+
for index, row in enumerate(self.df.itertuples()):
|
|
71
|
+
if row.allocation_end > 0:
|
|
72
|
+
label = f"{row.underlying_instrument__name_repr} {row.allocation_end*100:.1f}%"
|
|
73
|
+
pie_data.append(float(row.allocation_end))
|
|
74
|
+
pie.slices[index].fillColor = HexColor(row.color)
|
|
75
|
+
pie.slices[index].strokeColor = transparent
|
|
76
|
+
else:
|
|
77
|
+
label = f"{row.underlying_instrument__name_repr}"
|
|
78
|
+
pie_colornamepairs.append((HexColor(row.color), label))
|
|
79
|
+
max_width_colornamepair = max(max_width_colornamepair, stringWidth(label, "customfont", 6))
|
|
80
|
+
|
|
81
|
+
pie.slices.strokeWidth = -1
|
|
82
|
+
pie.data = pie_data
|
|
83
|
+
|
|
84
|
+
legend = CustomLegend()
|
|
85
|
+
|
|
86
|
+
legend.fontSize = 6
|
|
87
|
+
legend.fontName = "customfont"
|
|
88
|
+
legend.alignment = "right"
|
|
89
|
+
legend.boxAnchor = "nw"
|
|
90
|
+
legend.x = self.width / 2 - pie.width / 2 + self.pie_padding
|
|
91
|
+
legend.y = self.height - self.pie_padding
|
|
92
|
+
legend.dx = legend.dy = self.pie_legend_diameter
|
|
93
|
+
legend.columnMaximum = self.pie_legend_max_cols
|
|
94
|
+
legend.colorNamePairs = pie_colornamepairs
|
|
95
|
+
legend.strokeWidth = -1
|
|
96
|
+
|
|
97
|
+
legend.deltax = 0
|
|
98
|
+
legend.deltay = self.pie_legend_padding
|
|
99
|
+
legend.swdx = 12
|
|
100
|
+
legend.swdy = 0
|
|
101
|
+
|
|
102
|
+
return pie, legend
|
|
103
|
+
|
|
104
|
+
def get_bar_chart_with_legend(self):
|
|
105
|
+
bar = HorizontalBarChart()
|
|
106
|
+
|
|
107
|
+
self.df["contribution_total"] = self.df["contribution_total"] * 100
|
|
108
|
+
self.df["performance_total"] = self.df["performance_total"] * 100
|
|
109
|
+
self.df = self.df.sort_values(by=["performance_total"])
|
|
110
|
+
|
|
111
|
+
bar.data = [self.df.contribution_total.to_list(), self.df.performance_total.to_list()]
|
|
112
|
+
|
|
113
|
+
bar.x = 0
|
|
114
|
+
bar.y = 40
|
|
115
|
+
|
|
116
|
+
bar.width = self.width - self.bar_label_offset - stringWidth("00.00%", self.font_name, self.font_size)
|
|
117
|
+
bar.height = self.get_bar_chart_height()
|
|
118
|
+
|
|
119
|
+
bar.barLabels.dx = self.bar_label_offset
|
|
120
|
+
bar.barLabelFormat = DecimalFormatter(1, suffix="%")
|
|
121
|
+
bar.barLabels.dy = 0
|
|
122
|
+
bar.barLabels.boxAnchor = "w"
|
|
123
|
+
bar.barLabels.boxTarget = "hi"
|
|
124
|
+
bar.barLabels.fontSize = self.font_size
|
|
125
|
+
bar.barLabels.fontName = self.font_name
|
|
126
|
+
bar.barLabels.boxFillColor = None
|
|
127
|
+
bar.barLabels.boxStrokeColor = None
|
|
128
|
+
|
|
129
|
+
bar.groupSpacing = self.bar_padding
|
|
130
|
+
bar.barWidth = self.bar_width
|
|
131
|
+
bar.bars.strokeWidth = 0
|
|
132
|
+
bar.bars.strokeColor = None
|
|
133
|
+
|
|
134
|
+
for index, color in enumerate(self.df.color):
|
|
135
|
+
bar.bars[(0, index)].fillColor = HexColor(f"{color}80", hasAlpha=True)
|
|
136
|
+
bar.bars[(1, index)].fillColor = HexColor(color)
|
|
137
|
+
|
|
138
|
+
bar.valueAxis.labelTextFormat = DecimalFormatter(0, suffix="%")
|
|
139
|
+
bar.valueAxis.labels.fontName = "customfont"
|
|
140
|
+
bar.valueAxis.labels.fontSize = 6
|
|
141
|
+
bar.valueAxis.maximumTicks = 10
|
|
142
|
+
bar.valueAxis.strokeWidth = 0.5
|
|
143
|
+
bar.valueAxis.gridStrokeColor = self.grid_color
|
|
144
|
+
bar.valueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
|
|
145
|
+
bar.valueAxis.visibleGrid = True
|
|
146
|
+
bar.valueAxis.forceZero = True
|
|
147
|
+
|
|
148
|
+
bar.categoryAxis.tickLeft = 0
|
|
149
|
+
bar.categoryAxis.strokeWidth = 0.5
|
|
150
|
+
|
|
151
|
+
legend = CustomLegend()
|
|
152
|
+
legend.x = 30
|
|
153
|
+
legend.y = 0
|
|
154
|
+
|
|
155
|
+
legend.alignment = "right"
|
|
156
|
+
legend.boxAnchor = "sw"
|
|
157
|
+
legend.strokeWidth = -1
|
|
158
|
+
legend.columnMaximum = 1
|
|
159
|
+
legend.colorNamePairs = [
|
|
160
|
+
(HexColor(0xAAAAAA), "Monthly Performance"),
|
|
161
|
+
(HexColor(0xAAAAAA80, hasAlpha=True), "Monthly Contribution"),
|
|
162
|
+
]
|
|
163
|
+
legend.fontName = self.font_name
|
|
164
|
+
legend.fontSize = self.font_size
|
|
165
|
+
legend.deltax = 0
|
|
166
|
+
legend.swdx = 12
|
|
167
|
+
legend.swdy = 0
|
|
168
|
+
|
|
169
|
+
return bar, legend
|
|
170
|
+
|
|
171
|
+
def draw(self):
|
|
172
|
+
drawing = Drawing(self.width, self.height)
|
|
173
|
+
pie_chart, pie_legend = self.get_pie_chart_with_legend()
|
|
174
|
+
bar_chart, bar_legend = self.get_bar_chart_with_legend()
|
|
175
|
+
drawing.add(pie_chart)
|
|
176
|
+
drawing.add(pie_legend)
|
|
177
|
+
drawing.add(bar_chart)
|
|
178
|
+
drawing.add(bar_legend)
|
|
179
|
+
drawing.drawOn(self.canv, 0, 0)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from wbportfolio.models import Product
|
|
6
|
+
|
|
7
|
+
report = sys.argv[1]
|
|
8
|
+
|
|
9
|
+
module = importlib.import_module("wbreport.pdf.sandbox.templates.{report}")
|
|
10
|
+
product = Product.objects.get(id=sys.argv[2])
|
|
11
|
+
|
|
12
|
+
start = datetime.strptime(sys.argv[3], "%Y-%m-%d").date()
|
|
13
|
+
end = datetime.strptime(sys.argv[4], "%Y-%m-%d").date()
|
|
14
|
+
context = product.report._get_file_context(start=start, end=end)
|
|
15
|
+
result = module.generate_report(context)
|
|
16
|
+
with open("portfolio/report/pdf/sandbox/templates/testfile.pdf", "wb") as test_file:
|
|
17
|
+
test_file.write(result.read())
|
|
File without changes
|