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.
- inventree_manufacturing_costs-0.1.0.dist-info/METADATA +102 -0
- inventree_manufacturing_costs-0.1.0.dist-info/RECORD +25 -0
- inventree_manufacturing_costs-0.1.0.dist-info/WHEEL +5 -0
- inventree_manufacturing_costs-0.1.0.dist-info/entry_points.txt +2 -0
- inventree_manufacturing_costs-0.1.0.dist-info/licenses/LICENSE +21 -0
- inventree_manufacturing_costs-0.1.0.dist-info/top_level.txt +1 -0
- manufacturing_costs/__init__.py +3 -0
- manufacturing_costs/admin.py +23 -0
- manufacturing_costs/apps.py +13 -0
- manufacturing_costs/core.py +118 -0
- manufacturing_costs/migrations/0001_initial.py +163 -0
- manufacturing_costs/migrations/0002_remove_manufacturingcost_amortization_and_more.py +43 -0
- manufacturing_costs/migrations/0003_manufacturingcost_description.py +22 -0
- manufacturing_costs/migrations/0004_manufacturingcost_active_manufacturingcost_inherited.py +31 -0
- manufacturing_costs/migrations/__init__.py +0 -0
- manufacturing_costs/models.py +166 -0
- manufacturing_costs/serializers.py +136 -0
- manufacturing_costs/static/.vite/manifest.json +24 -0
- manufacturing_costs/static/AdminPanel.js +192 -0
- manufacturing_costs/static/AdminPanel.js.map +1 -0
- manufacturing_costs/static/PartPanel.js +333 -0
- manufacturing_costs/static/PartPanel.js.map +1 -0
- manufacturing_costs/static/assets/index-BjwOiYns.js +2744 -0
- manufacturing_costs/static/assets/index-BjwOiYns.js.map +1 -0
- manufacturing_costs/views.py +276 -0
|
@@ -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
|
+
]
|