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.

Files changed (70) hide show
  1. wbreport/__init__.py +1 -0
  2. wbreport/admin.py +87 -0
  3. wbreport/apps.py +6 -0
  4. wbreport/defaults/__init__.py +0 -0
  5. wbreport/defaults/factsheets/__init__.py +0 -0
  6. wbreport/defaults/factsheets/base.py +990 -0
  7. wbreport/defaults/factsheets/menu.py +93 -0
  8. wbreport/defaults/factsheets/mixins.py +35 -0
  9. wbreport/defaults/factsheets/multitheme.py +947 -0
  10. wbreport/dynamic_preferences_registry.py +15 -0
  11. wbreport/factories/__init__.py +8 -0
  12. wbreport/factories/data_classes.py +48 -0
  13. wbreport/factories/reports.py +79 -0
  14. wbreport/filters.py +37 -0
  15. wbreport/fixtures/wbreport.yaml +1 -0
  16. wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
  17. wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
  18. wbreport/migrations/0009_alter_report_color_palette.py +27 -0
  19. wbreport/migrations/0010_auto_20240103_0947.py +43 -0
  20. wbreport/migrations/0011_auto_20240207_1629.py +35 -0
  21. wbreport/migrations/0012_reportversion_lock.py +17 -0
  22. wbreport/migrations/0013_alter_reportversion_context.py +18 -0
  23. wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
  24. wbreport/migrations/__init__.py +0 -0
  25. wbreport/mixins.py +183 -0
  26. wbreport/models.py +781 -0
  27. wbreport/pdf/__init__.py +0 -0
  28. wbreport/pdf/charts/__init__.py +0 -0
  29. wbreport/pdf/charts/legend.py +15 -0
  30. wbreport/pdf/charts/pie.py +169 -0
  31. wbreport/pdf/charts/timeseries.py +77 -0
  32. wbreport/pdf/flowables/risk.py +88 -0
  33. wbreport/pdf/flowables/textboxes.py +143 -0
  34. wbreport/pdf/flowables/themes.py +179 -0
  35. wbreport/pdf/sandbox/__init__.py +0 -0
  36. wbreport/pdf/sandbox/run.py +17 -0
  37. wbreport/pdf/sandbox/templates/__init__.py +0 -0
  38. wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
  39. wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
  40. wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
  41. wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
  42. wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
  43. wbreport/pdf/tables/__init__.py +0 -0
  44. wbreport/pdf/tables/aggregated_tables.py +156 -0
  45. wbreport/pdf/tables/data_tables.py +75 -0
  46. wbreport/serializers.py +191 -0
  47. wbreport/tasks.py +60 -0
  48. wbreport/templates/__init__.py +0 -0
  49. wbreport/templatetags/__init__.py +0 -0
  50. wbreport/templatetags/portfolio_tags.py +35 -0
  51. wbreport/tests/__init__.py +0 -0
  52. wbreport/tests/conftest.py +24 -0
  53. wbreport/tests/test_models.py +253 -0
  54. wbreport/tests/test_tasks.py +17 -0
  55. wbreport/tests/test_viewsets.py +0 -0
  56. wbreport/tests/tests.py +12 -0
  57. wbreport/urls.py +29 -0
  58. wbreport/urls_public.py +10 -0
  59. wbreport/viewsets/__init__.py +10 -0
  60. wbreport/viewsets/configs/__init__.py +18 -0
  61. wbreport/viewsets/configs/buttons.py +193 -0
  62. wbreport/viewsets/configs/displays.py +116 -0
  63. wbreport/viewsets/configs/endpoints.py +23 -0
  64. wbreport/viewsets/configs/menus.py +8 -0
  65. wbreport/viewsets/configs/titles.py +30 -0
  66. wbreport/viewsets/viewsets.py +330 -0
  67. wbreport-1.43.1.dist-info/METADATA +7 -0
  68. wbreport-1.43.1.dist-info/RECORD +70 -0
  69. wbreport-1.43.1.dist-info/WHEEL +5 -0
  70. wbreport-1.43.1.dist-info/licenses/LICENSE +4 -0
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