inventree-manufacturing-costs 0.1.0__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.
@@ -0,0 +1,276 @@
1
+ """API views for the ManufacturingCosts plugin."""
2
+
3
+ from decimal import Decimal
4
+ from typing import cast
5
+
6
+ import tablib
7
+
8
+ from django.db.models import Q
9
+ from django_filters import rest_framework as rest_filters
10
+ from rest_framework import filters, permissions
11
+ from rest_framework.views import APIView
12
+
13
+ from InvenTree.helpers import DownloadFile
14
+ from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI
15
+ import part.models
16
+
17
+ from .models import ManufacturingRate, ManufacturingCost
18
+ from .serializers import (
19
+ ManufacturingRateSerializer,
20
+ ManufacturingCostSerializer,
21
+ AssemblyCostRequestSerializer,
22
+ )
23
+
24
+
25
+ class ManufacturingRateMixin:
26
+ """Mixin class for ManufacturingRate API endpoints."""
27
+
28
+ # TODO: Fix up the permissions and authentication for this mixin
29
+ permission_classes = [permissions.IsAuthenticated]
30
+ serializer_class = ManufacturingRateSerializer
31
+ queryset = ManufacturingRate.objects.all()
32
+
33
+
34
+ class ManufacturingRateList(ManufacturingRateMixin, ListCreateAPI):
35
+ """API endpoint for listing and creating ManufacturingRate instances."""
36
+
37
+ filter_backends = [filters.OrderingFilter, filters.SearchFilter]
38
+
39
+ ordering_fields = [
40
+ "pk",
41
+ "name",
42
+ "units",
43
+ ]
44
+
45
+ search_fields = [
46
+ "name",
47
+ "description",
48
+ ]
49
+
50
+
51
+ class ManufacturingRateDetail(ManufacturingRateMixin, RetrieveUpdateDestroyAPI):
52
+ """API endpoint for retrieving, updating, and deleting a ManufacturingRate instance."""
53
+
54
+ ...
55
+
56
+
57
+ class ManufacturingCostMixin:
58
+ """Mixin class for ManufacturingCost API endpoints."""
59
+
60
+ permission_classes = [permissions.IsAuthenticated]
61
+ serializer_class = ManufacturingCostSerializer
62
+ queryset = ManufacturingCost.objects.all()
63
+
64
+ def get_queryset(self):
65
+ """Return the queryset for the ManufacturingRate model."""
66
+ queryset = super().get_queryset()
67
+ queryset = queryset.prefetch_related("part")
68
+
69
+ return queryset
70
+
71
+
72
+ class ManufacturingCostFilter(rest_filters.FilterSet):
73
+ """Filter class for ManufacturingCost API endpoints."""
74
+
75
+ class Meta:
76
+ model = ManufacturingCost
77
+ fields = [
78
+ "active",
79
+ "inherited",
80
+ ]
81
+
82
+ part = rest_filters.ModelChoiceFilter(
83
+ queryset=part.models.Part.objects.all(), label="Part", method="filter_part"
84
+ )
85
+
86
+ def filter_part(self, queryset, name, part):
87
+ """Filter ManufacturingCost instances by part."""
88
+
89
+ parts = part.get_ancestors(include_self=True)
90
+ Q1 = Q(part__in=parts, inherited=True)
91
+ Q2 = Q(part=part, inherited=False)
92
+
93
+ return queryset.filter(Q1 | Q2).distinct()
94
+
95
+
96
+ class ManufacturingCostList(ManufacturingCostMixin, ListCreateAPI):
97
+ """API endpoint for listing and creating ManufacturingCost instances."""
98
+
99
+ filterset_class = ManufacturingCostFilter
100
+
101
+ filter_backends = [
102
+ rest_filters.DjangoFilterBackend,
103
+ filters.OrderingFilter,
104
+ filters.SearchFilter,
105
+ ]
106
+
107
+ ordering_fields = ["pk", "part", "rate", "quantity"]
108
+
109
+ search_fields = [
110
+ "part__name",
111
+ "part__IPN",
112
+ "rate__name",
113
+ "rate__description",
114
+ "notes",
115
+ ]
116
+
117
+
118
+ class ManufacturingCostDetail(ManufacturingCostMixin, RetrieveUpdateDestroyAPI):
119
+ """API endpoint for retrieving, updating, and deleting a ManufacturingCost instance."""
120
+
121
+ ...
122
+
123
+
124
+ class AssemblyCostExport(APIView):
125
+ """API endpoint for exporting ManufacturingCost data for a single assembly."""
126
+
127
+ permission_classes = [permissions.IsAuthenticated]
128
+
129
+ def get(self, request):
130
+ """Export manufacturing cost data for a given assembly part."""
131
+
132
+ serializer = AssemblyCostRequestSerializer(data=request.query_params)
133
+
134
+ serializer.is_valid(raise_exception=True)
135
+ data = cast(dict, serializer.validated_data)
136
+
137
+ self.part = data["part"]
138
+ self.include_subassemblies = data.get("include_subassemblies", True)
139
+ self.export_format = data.get("export_format", "csv")
140
+
141
+ return self.export_data()
142
+
143
+ def export_data(self):
144
+ """Construct a dataset for export."""
145
+
146
+ headers = self.file_headers()
147
+ self.dataset = tablib.Dataset(headers=map(str, headers))
148
+
149
+ # Start the processing with the top-level assembly
150
+ self.process_assembly(self.part)
151
+
152
+ data = self.dataset.export(self.export_format)
153
+
154
+ return DownloadFile(
155
+ data,
156
+ filename=f"assembly_costs_{self.part.full_name}.{self.export_format}",
157
+ )
158
+
159
+ def file_headers(self):
160
+ """Return the headers for the exported dataset."""
161
+
162
+ return [
163
+ "BOM Level",
164
+ "Quantity Multiplier",
165
+ "Part ID",
166
+ "Part IPN",
167
+ "Part Name",
168
+ "Rate",
169
+ "Rate Description",
170
+ "Cost",
171
+ "Notes",
172
+ "Base Quantity",
173
+ "Total Quantity",
174
+ "Unit Cost",
175
+ "Total Cost",
176
+ "Currency",
177
+ ]
178
+
179
+ def find_costs_for_assembly(self, part):
180
+ """Find all ManufacturingCost entries for a given assembly part.
181
+
182
+ These may be direct costs, or inherited costs from template parts.
183
+ """
184
+
185
+ parents = part.get_ancestors(include_self=True)
186
+ Q1 = Q(part__in=parents, inherited=True)
187
+ Q2 = Q(part=part, inherited=False)
188
+
189
+ costs = ManufacturingCost.objects.filter(active=True).filter(Q1 | Q2).distinct()
190
+ costs = costs.prefetch_related("part", "rate")
191
+
192
+ return costs
193
+
194
+ def process_assembly(self, part, level: int = 1, multiplier: Decimal = Decimal(1)):
195
+ """Process an assembly part and its sub-assemblies to populate the dataset."""
196
+
197
+ base_row_data = [
198
+ level,
199
+ multiplier,
200
+ part.pk,
201
+ part.IPN,
202
+ part.name,
203
+ ]
204
+
205
+ costs = self.find_costs_for_assembly(part)
206
+
207
+ for cost in costs:
208
+ unit_cost = cost.calculate_cost(1.0)
209
+
210
+ row = [
211
+ *base_row_data,
212
+ cost.rate.name if cost.rate else "-",
213
+ cost.rate.description if cost.rate else "-",
214
+ cost.description,
215
+ cost.notes,
216
+ float(cost.quantity),
217
+ float(cost.quantity * multiplier),
218
+ float(unit_cost.amount),
219
+ float(unit_cost.amount * cost.quantity * multiplier),
220
+ str(unit_cost.currency),
221
+ ]
222
+
223
+ # Add this row to the dataset
224
+ self.dataset.append(row)
225
+
226
+ # Process sub-assemblies as required
227
+ if self.include_subassemblies:
228
+ # Find all subassemblies
229
+ bom_items = part.get_bom_items().filter(sub_part__assembly=True)
230
+
231
+ for bom_item in bom_items:
232
+ self.process_assembly(
233
+ bom_item.sub_part,
234
+ level=level + 1,
235
+ multiplier=multiplier * bom_item.quantity,
236
+ )
237
+
238
+
239
+ def construct_urls():
240
+ """Construct the URL patterns for the ManufacturingCosts plugin."""
241
+
242
+ from django.urls import path, include
243
+
244
+ return [
245
+ path(
246
+ "rate/",
247
+ include([
248
+ path(
249
+ "<int:pk>/",
250
+ ManufacturingRateDetail.as_view(),
251
+ name="manufacturing-rate-detail",
252
+ ),
253
+ path(
254
+ "", ManufacturingRateList.as_view(), name="manufacturing-rate-list"
255
+ ),
256
+ ]),
257
+ ),
258
+ path(
259
+ "cost/",
260
+ include([
261
+ path(
262
+ "export/",
263
+ AssemblyCostExport.as_view(),
264
+ name="assembly-cost-export",
265
+ ),
266
+ path(
267
+ "<int:pk>/",
268
+ ManufacturingCostDetail.as_view(),
269
+ name="manufacturing-cost-detail",
270
+ ),
271
+ path(
272
+ "", ManufacturingCostList.as_view(), name="manufacturing-cost-list"
273
+ ),
274
+ ]),
275
+ ),
276
+ ]