inventree-component-shortfall 0.1.0__tar.gz

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.
Files changed (20) hide show
  1. inventree_component_shortfall-0.1.0/LICENSE +21 -0
  2. inventree_component_shortfall-0.1.0/MANIFEST.in +2 -0
  3. inventree_component_shortfall-0.1.0/PKG-INFO +41 -0
  4. inventree_component_shortfall-0.1.0/README.md +25 -0
  5. inventree_component_shortfall-0.1.0/component_shortfall/__init__.py +3 -0
  6. inventree_component_shortfall-0.1.0/component_shortfall/core.py +183 -0
  7. inventree_component_shortfall-0.1.0/component_shortfall/serializers.py +46 -0
  8. inventree_component_shortfall-0.1.0/component_shortfall/shortfall.py +324 -0
  9. inventree_component_shortfall-0.1.0/component_shortfall/static/.vite/manifest.json +8 -0
  10. inventree_component_shortfall-0.1.0/component_shortfall/static/Dashboard.js +85 -0
  11. inventree_component_shortfall-0.1.0/component_shortfall/static/Dashboard.js.map +1 -0
  12. inventree_component_shortfall-0.1.0/component_shortfall/templates/component_shortfall/shortfall_email.html +35 -0
  13. inventree_component_shortfall-0.1.0/component_shortfall/views.py +58 -0
  14. inventree_component_shortfall-0.1.0/inventree_component_shortfall.egg-info/PKG-INFO +41 -0
  15. inventree_component_shortfall-0.1.0/inventree_component_shortfall.egg-info/SOURCES.txt +19 -0
  16. inventree_component_shortfall-0.1.0/inventree_component_shortfall.egg-info/dependency_links.txt +1 -0
  17. inventree_component_shortfall-0.1.0/inventree_component_shortfall.egg-info/entry_points.txt +2 -0
  18. inventree_component_shortfall-0.1.0/inventree_component_shortfall.egg-info/top_level.txt +3 -0
  19. inventree_component_shortfall-0.1.0/pyproject.toml +53 -0
  20. inventree_component_shortfall-0.1.0/setup.cfg +19 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Oliver Walters <oliver.henry.walters@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ recursive-include component_shortfall/static *
2
+ recursive-include component_shortfall/templates *
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: inventree-component-shortfall
3
+ Version: 0.1.0
4
+ Summary: Generate component shortfall reports
5
+ Author-email: Oliver Walters <oliver.henry.walters@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SchrodingersGat/inventree-shortfall-report
8
+ Keywords: inventree,plugin
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: InvenTree
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # ComponentShortfall
18
+
19
+ Generate component shortfall reports
20
+
21
+ ## Installation
22
+
23
+ ### InvenTree Plugin Manager
24
+
25
+ ... todo ...
26
+
27
+ ### Command Line
28
+
29
+ To install manually via the command line, run the following command:
30
+
31
+ ```bash
32
+ pip install inventree-component-shortfall
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ ... todo ...
38
+
39
+ ## Usage
40
+
41
+ ... todo ...
@@ -0,0 +1,25 @@
1
+ # ComponentShortfall
2
+
3
+ Generate component shortfall reports
4
+
5
+ ## Installation
6
+
7
+ ### InvenTree Plugin Manager
8
+
9
+ ... todo ...
10
+
11
+ ### Command Line
12
+
13
+ To install manually via the command line, run the following command:
14
+
15
+ ```bash
16
+ pip install inventree-component-shortfall
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ ... todo ...
22
+
23
+ ## Usage
24
+
25
+ ... todo ...
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ PLUGIN_VERSION = "0.1.0"
@@ -0,0 +1,183 @@
1
+ """Generate component shortfall reports"""
2
+
3
+ from plugin import InvenTreePlugin
4
+
5
+ from plugin.mixins import ScheduleMixin, SettingsMixin, UrlsMixin, UserInterfaceMixin
6
+
7
+ from . import PLUGIN_VERSION
8
+
9
+
10
+ class ComponentShortfall(
11
+ ScheduleMixin, SettingsMixin, UrlsMixin, UserInterfaceMixin, InvenTreePlugin
12
+ ):
13
+ """ComponentShortfall - custom InvenTree plugin."""
14
+
15
+ # Plugin metadata
16
+ TITLE = "Component Shortfall"
17
+ NAME = "ComponentShortfall"
18
+ SLUG = "component-shortfall"
19
+ DESCRIPTION = "Generate component shortfall reports"
20
+ VERSION = PLUGIN_VERSION
21
+
22
+ # Additional project information
23
+ AUTHOR = "Oliver Walters"
24
+ WEBSITE = "https://github.com/SchrodingersGat/inventree-shortfall-report"
25
+ LICENSE = "MIT"
26
+
27
+ # Optionally specify supported InvenTree versions
28
+ # MIN_VERSION = '0.18.0'
29
+ # MAX_VERSION = '2.0.0'
30
+
31
+ SCHEDULED_TASKS = {
32
+ "shortfall_report": {"func": "periodic_shortfall_report", "schedule": "D"}
33
+ }
34
+
35
+ # Plugin settings (from SettingsMixin)
36
+ # Ref: https://docs.inventree.org/en/latest/plugins/mixins/settings/
37
+ SETTINGS = {
38
+ "HIDE_NO_SHORTFALL": {
39
+ "name": "Hide No Shortfall",
40
+ "description": "Hide results for parts which have no shortfall",
41
+ "default": True,
42
+ "validator": bool,
43
+ },
44
+ "SHORTFALL_REPORT_DAYS": {
45
+ "name": "Shortfall Report Days",
46
+ "description": "Number of days between automatic shortfall report generation (set to 0 to disable)",
47
+ "default": 7,
48
+ "validator": int,
49
+ },
50
+ "SHORTFALL_REPORT_GROUP": {
51
+ "name": "Shortfall Report Group",
52
+ "description": "User group to send periodic shortfall reports",
53
+ "model": "auth.group",
54
+ },
55
+ }
56
+
57
+ # Custom URL endpoints (from UrlsMixin)
58
+ # Ref: https://docs.inventree.org/en/latest/plugins/mixins/urls/
59
+ def setup_urls(self):
60
+ """Configure custom URL endpoints for this plugin."""
61
+ from django.urls import path
62
+ from .views import ShortfallReportView
63
+
64
+ return [
65
+ # Provide path to a simple custom view - replace this with your own views
66
+ path(
67
+ "shortfall/",
68
+ ShortfallReportView.as_view(),
69
+ name="shortfall-report-view",
70
+ ),
71
+ ]
72
+
73
+ # User interface elements (from UserInterfaceMixin)
74
+ # Ref: https://docs.inventree.org/en/latest/plugins/mixins/ui/
75
+
76
+ # Custom dashboard items
77
+ def get_ui_dashboard_items(self, request, context: dict, **kwargs):
78
+ """Return a list of custom dashboard items to be rendered in the InvenTree user interface."""
79
+
80
+ # Example: only display for 'staff' users
81
+ if not request.user or not request.user.is_staff:
82
+ return []
83
+
84
+ items = []
85
+
86
+ items.append({
87
+ "key": "component-shortfall-dashboard",
88
+ "title": "Shortfall Report",
89
+ "description": "Generate a component shortfall report",
90
+ "icon": "ti:clipboard-check:outline",
91
+ "source": self.plugin_static_file(
92
+ "Dashboard.js:renderComponentShortfallDashboardItem"
93
+ ),
94
+ })
95
+
96
+ return items
97
+
98
+ def get_ui_spotlight_actions(self, request, context, **kwargs):
99
+ """Return a list of custom spotlight actions to be made available."""
100
+ return [
101
+ {
102
+ "key": "shortfall-action",
103
+ "title": "Shortfall Report",
104
+ "description": "Generate a component shortfall report",
105
+ "icon": "ti:clipboard-check:outline",
106
+ "source": self.plugin_static_file(
107
+ "Spotlight.js:ComponentShortfallSpotlightAction"
108
+ ),
109
+ }
110
+ ]
111
+
112
+ def periodic_shortfall_report(self):
113
+ """Scheduled task to periodically generate a shortfall report.
114
+
115
+ This task is called daily, but uses the SHORTFALL_REPORT_DAYS setting to determine how often to actually generate the report.
116
+ """
117
+
118
+ import InvenTree.helpers_email
119
+ import InvenTree.tasks
120
+ from common.models import DataOutput
121
+ from .shortfall import calculate_shortfall, format_shortfall_report_html
122
+ from django.contrib.auth.models import Group
123
+
124
+ report_period = int(self.get_setting("SHORTFALL_REPORT_DAYS"))
125
+
126
+ if report_period <= 0:
127
+ return
128
+
129
+ if not InvenTree.tasks.check_daily_holdoff(
130
+ "component_shortfall_report", report_period
131
+ ):
132
+ return
133
+
134
+ # Run the report generation task here
135
+ data_output = DataOutput.objects.create(
136
+ user=None,
137
+ total=0,
138
+ progress=0,
139
+ output_type="shortfall_report",
140
+ plugin=self.SLUG,
141
+ )
142
+
143
+ hide_no_shortfall = self.get_setting("HIDE_NO_SHORTFALL")
144
+
145
+ # Calculate shortfall report with default settings
146
+ requirements = calculate_shortfall(data_output.pk)
147
+
148
+ data_output.refresh_from_db()
149
+
150
+ # Email the report to interested users?
151
+ report_group_id = self.get_setting("SHORTFALL_REPORT_GROUP")
152
+
153
+ users = []
154
+
155
+ try:
156
+ group = Group.objects.get(pk=report_group_id)
157
+ users = group.user_set.filter(is_active=True)
158
+ except Group.DoesNotExist:
159
+ pass
160
+
161
+ recipients = []
162
+
163
+ for user in users:
164
+ if email := InvenTree.helpers_email.get_email_for_user(user):
165
+ if email not in recipients:
166
+ recipients.append(email)
167
+
168
+ # Construct the email body
169
+ body = format_shortfall_report_html(
170
+ requirements, data_output, hide_no_shortfall=hide_no_shortfall
171
+ )
172
+
173
+ # Send email to users, with the report attached
174
+ if recipients:
175
+ InvenTree.helpers_email.send_email(
176
+ subject="[InvenTree] Component Shortfall Report",
177
+ body="Please find the attached component shortfall report.",
178
+ html_message=body,
179
+ recipients=recipients,
180
+ )
181
+
182
+ # Record success for the task
183
+ InvenTree.tasks.record_task_success("component_shortfall_report")
@@ -0,0 +1,46 @@
1
+ """API serializers for the ComponentShortfall plugin.
2
+
3
+ In practice, you would define your custom serializers here.
4
+
5
+ Ref: https://www.django-rest-framework.org/api-guide/serializers/
6
+ """
7
+
8
+ from django.utils.translation import gettext_lazy as _
9
+ from rest_framework import serializers
10
+
11
+ import common.serializers
12
+ import part.models as part_models
13
+
14
+
15
+ class ShortfallReportSerializer(serializers.Serializer):
16
+ """Serializer for shortfall report request parameters."""
17
+
18
+ category = serializers.PrimaryKeyRelatedField(
19
+ queryset=part_models.PartCategory.objects.all(),
20
+ many=False,
21
+ required=False,
22
+ allow_null=True,
23
+ label=_("Category"),
24
+ help_text=_("The category for which to retrieve shortfall data"),
25
+ )
26
+
27
+ output = common.serializers.DataOutputSerializer(
28
+ read_only=True,
29
+ allow_null=True,
30
+ )
31
+
32
+ max_bom_depth = serializers.IntegerField(
33
+ required=False,
34
+ default=50,
35
+ min_value=0,
36
+ max_value=50,
37
+ label=_("Maximum BOM Depth"),
38
+ help_text=_("The maximum depth to traverse the BOM when calculating shortfall"),
39
+ )
40
+
41
+ def validate(self, data):
42
+ """Validate the provided data."""
43
+
44
+ # TODO: Any custom data validation goes here
45
+
46
+ return data
@@ -0,0 +1,324 @@
1
+ """Functions for determining component shortfall.
2
+
3
+ Process Goals:
4
+
5
+ - Determine the overall "requirements" - based on outstanding Sales Orders
6
+ - Iterate downward through the BOMs for each top-level part, to determine the requirements for each sub-component
7
+ - Aggregate the "total" requirements for each component (based on the requirements of all parent assemblies)
8
+ - Determine the shortfall for each component, based on the available stock, on-order quantity and in-production quantity
9
+
10
+ """
11
+
12
+ from typing import Optional
13
+ from decimal import Decimal
14
+ import os
15
+ import structlog
16
+ import tablib
17
+
18
+ from django.core.files.base import ContentFile
19
+ from django.db.models import F
20
+
21
+ from InvenTree.helpers_model import construct_absolute_url
22
+
23
+ import common.models as common_models
24
+ import part.models as part_models
25
+
26
+
27
+ logger = structlog.get_logger("inventree.shortfall")
28
+
29
+
30
+ def update_part_requirements(
31
+ part, required_qty: Decimal, component_data: dict
32
+ ) -> Decimal:
33
+ """Return requirements for the given part.
34
+
35
+ Arguments:
36
+ part: The part to process
37
+ required_qty: The additional quantity required for the part
38
+ component_data: A dict of part requirements (may be updated)
39
+
40
+ Returns:
41
+ The *additional* shortfall for this part (not cumulative)
42
+ """
43
+
44
+ requirements = component_data.get(part.pk, None) or {}
45
+
46
+ # Store the part information against the part
47
+ requirements["part"] = part
48
+
49
+ # Fetch (or calculate) the various stock values for this part
50
+ if "stock" not in requirements:
51
+ requirements["stock"] = part.get_stock_count(include_variants=False)
52
+
53
+ # TODO: What about BOM items which allow variants???
54
+
55
+ if "on_order" not in requirements:
56
+ requirements["on_order"] = part.on_order
57
+
58
+ # Add in the additional requirements
59
+ requirements["required"] = requirements.get("required", Decimal(0)) + Decimal(
60
+ required_qty
61
+ )
62
+
63
+ # TODO: Support offset for "in production" quantity
64
+
65
+ initial_shortfall = requirements.get("shortfall", Decimal(0))
66
+
67
+ # Calculate the "shortfall" for this part
68
+ requirements["shortfall"] = max(
69
+ 0, requirements["required"] - requirements["stock"] - requirements["on_order"]
70
+ )
71
+
72
+ # Update the global dict of component data
73
+ component_data[part.pk] = requirements
74
+
75
+ # Return the additional shortfall for this part
76
+ return requirements["shortfall"] - initial_shortfall
77
+
78
+
79
+ def get_outstanding_parts(category: Optional[part_models.PartCategory] = None) -> dict:
80
+ """Return a dict of outstanding parts (based on open sales orders).
81
+
82
+ Returns a dict of part requirements, with the part ID as the key.
83
+
84
+ Each element in the dict has the follow values:
85
+ - part: The part object
86
+ - required: The required quantity of the part (for sales order and build orders)
87
+
88
+ Arguments:
89
+ - category: Optional category to filter the parts by
90
+ """
91
+
92
+ from order.models import SalesOrderLineItem
93
+ from order.status_codes import SalesOrderStatusGroups
94
+
95
+ # Find all open sales order line items which are not completed
96
+ sales_order_lines = SalesOrderLineItem.objects.filter(
97
+ order__status__in=SalesOrderStatusGroups.OPEN,
98
+ part__virtual=False,
99
+ shipped__lt=F("quantity"),
100
+ ).prefetch_related(
101
+ "part",
102
+ )
103
+
104
+ # TODO: Filter by order status (e.g. exclude pending orders)
105
+
106
+ # TODO: Filter by order date (e.g. only include orders which are due within a certain time frame)
107
+
108
+ # Filter by part category (e.g. only include orders for parts within a certain category)
109
+ if category:
110
+ categories = category.get_descendants(include_self=True)
111
+ sales_order_lines = sales_order_lines.filter(part__category__in=categories)
112
+
113
+ outstanding_parts = {}
114
+
115
+ for line in sales_order_lines:
116
+ defecit = max(0, line.quantity - line.shipped)
117
+
118
+ if defecit <= 0:
119
+ # No outstanding quantity for this line item
120
+ continue
121
+
122
+ part_data = outstanding_parts.get(line.part.pk, None) or {
123
+ "part": line.part,
124
+ "required": Decimal(0),
125
+ }
126
+ part_data["required"] += line.quantity - line.shipped
127
+ outstanding_parts[line.part.pk] = part_data
128
+
129
+ return outstanding_parts
130
+
131
+
132
+ def calculate_shortfall(
133
+ output_id: int, category_id: Optional[int] = None, max_bom_depth: int = 50
134
+ ) -> dict:
135
+ """Calculate the component shortfall for a given list of component IDs.
136
+
137
+ Arguments:
138
+ output_id: The ID of the DataOutput (where to save the results)
139
+ max_bom_depth: The maximum depth to traverse the BOM when calculating shortfall (default: 50)
140
+ category_id: The ID of the category to filter parts by (optional)
141
+
142
+ Returns:
143
+ A dict of part requirements, with the part ID as the key.
144
+
145
+ Each element in the dict has the follow values:
146
+ - part: The part object
147
+ - required: The required quantity of the part (for sales order and build orders)
148
+ - stock: The current stock on hand for this part
149
+ - on_order: The quantity of this part currently on order
150
+ - shortfall: The calculated shortfall for this part (required - stock - on_order)
151
+ """
152
+
153
+ logger.info("Generating component shortfall report")
154
+
155
+ try:
156
+ data_output = common_models.DataOutput.objects.get(pk=output_id)
157
+ except common_models.DataOutput.DoesNotExist:
158
+ logger.error(
159
+ f"component_shortfall: DataOutput with ID {output_id} does not exist - cannot save results"
160
+ )
161
+ return
162
+
163
+ try:
164
+ if category_id:
165
+ category = part_models.PartCategory.objects.get(pk=category_id)
166
+ else:
167
+ category = None
168
+ except (ValueError, part_models.PartCategory.DoesNotExist):
169
+ logger.warning(
170
+ f"component_shortfall: PartCategory with ID {category_id} does not exist - cannot filter parts"
171
+ )
172
+ category = None
173
+
174
+ # First, determine the set of components which are "on order"
175
+ initial_parts = get_outstanding_parts(category=category)
176
+
177
+ # Let's keep track of all the requirements, top-to-bottom, in a single dict - keyed by part ID
178
+ # key: part ID
179
+ # - part: Part instance
180
+ # - required: Total required quantity for this part (cumulative)
181
+ # - stock: Current stock on hand for this part
182
+ # - on_order: Quantity of this part currently on order
183
+ # - building: Quantity of this part currently being built
184
+ requirements = {}
185
+
186
+ # Keep a list of the components still required to process - start with the initial set of outstanding parts
187
+ # Each entry is a tuple of (part, quantity, level)
188
+ components_to_process = []
189
+
190
+ # Start with the initial set of outstanding parts
191
+ for _, data in initial_parts.items():
192
+ part = data["part"]
193
+ required_qty = data["required"]
194
+
195
+ components_to_process.append((part, required_qty, 0))
196
+
197
+ # Update initial conditions for the data output
198
+ data_output.progress = 0
199
+ data_output.total = len(components_to_process)
200
+ data_output.save()
201
+
202
+ while components_to_process:
203
+ part, quantity, level = components_to_process.pop(0)
204
+ data_output.progress += 1
205
+
206
+ shortfall = update_part_requirements(part, quantity, requirements)
207
+
208
+ # Update every 50 iterations
209
+ if data_output.progress % 50 == 0:
210
+ data_output.save()
211
+
212
+ if shortfall <= 0:
213
+ # No shortfall for this part - skip processing any sub-components
214
+ continue
215
+
216
+ # Prevent deep recursion into the BOM - if we have reached the maximum level, then we will not process any sub-components
217
+ if level >= max_bom_depth:
218
+ continue
219
+
220
+ # Is this an assembly?
221
+ if part.assembly:
222
+ components = (
223
+ part.get_bom_items(include_virtual=False)
224
+ .filter(consumable=False)
225
+ .prefetch_related(
226
+ "sub_part",
227
+ "sub_part__category",
228
+ )
229
+ )
230
+
231
+ for item in components:
232
+ sub_part = item.sub_part
233
+
234
+ # Calculate the quantity multiplier for this sub-part
235
+ required_qty = item.get_required_quantity(shortfall)
236
+
237
+ components_to_process.append((sub_part, required_qty, level + 1))
238
+ data_output.total += 1
239
+
240
+ # Generate the output data file
241
+ headers = [
242
+ "Part ID",
243
+ "Part Name",
244
+ "Part IPN",
245
+ "Category ID",
246
+ "Category Name",
247
+ "Current Stock",
248
+ "On Order",
249
+ "Required Quantity",
250
+ "Shortfall",
251
+ ]
252
+
253
+ dataset = tablib.Dataset(headers=headers)
254
+
255
+ for _, data in requirements.items():
256
+ part = data["part"]
257
+ url = construct_absolute_url(part.get_absolute_url())
258
+
259
+ row = [
260
+ f'=HYPERLINK("{url}", "{part.pk}")',
261
+ part.name,
262
+ part.IPN,
263
+ part.category.pk if part.category else None,
264
+ part.category.pathstring if part.category else None,
265
+ data["stock"],
266
+ data["on_order"],
267
+ data["required"],
268
+ data["shortfall"],
269
+ ]
270
+ dataset.append(row)
271
+
272
+ # Attach the generated file to the data output
273
+ datafile = dataset.export("xlsx")
274
+
275
+ data_output.mark_complete(
276
+ output=ContentFile(datafile, name="shortfall_report.xlsx")
277
+ )
278
+
279
+ return requirements
280
+
281
+
282
+ def format_shortfall_report_html(
283
+ requirements: dict, output: common_models.DataOutput, hide_no_shortfall: bool = True
284
+ ) -> str:
285
+ """Format the shortfall report as a HTML document."""
286
+
287
+ from django.template import Template, Context
288
+
289
+ file_path = os.path.join(
290
+ os.path.dirname(__file__),
291
+ "templates",
292
+ "component_shortfall",
293
+ "shortfall_email.html",
294
+ )
295
+
296
+ with open(file_path, "r") as f:
297
+ template_content = f.read()
298
+
299
+ context_data = {}
300
+
301
+ # Add download link
302
+ if output and output.output:
303
+ context_data["download_url"] = construct_absolute_url(output.output.url)
304
+
305
+ # Add all the requirements entries
306
+ requirements_list = []
307
+
308
+ for entry in requirements.values():
309
+ if hide_no_shortfall and entry.get("shortfall", 0) <= 0:
310
+ continue
311
+
312
+ requirements_list.append({
313
+ **entry,
314
+ "part_url": construct_absolute_url(entry["part"].get_absolute_url()),
315
+ })
316
+
317
+ context_data["requirements"] = requirements_list
318
+
319
+ template = Template(template_content)
320
+ context = Context(context_data)
321
+
322
+ data = template.render(context)
323
+
324
+ return data
@@ -0,0 +1,8 @@
1
+ {
2
+ "src/Dashboard.tsx": {
3
+ "file": "Dashboard.js",
4
+ "name": "Dashboard",
5
+ "src": "src/Dashboard.tsx",
6
+ "isEntry": true
7
+ }
8
+ }