23518f7a0673R71">71
+                ('MI39', models.CharField(max_length=255, null=True)),
72
+                ('MI24', models.CharField(max_length=255, null=True)),
73
+                ('Ind1', models.CharField(max_length=255, null=True)),
74
+                ('Ind2', models.CharField(max_length=255, null=True)),
75
+                ('Ind3', models.CharField(max_length=255, null=True)),
76
+                ('AGR1', models.CharField(max_length=255, null=True)),
77
+                ('AGR2', models.CharField(max_length=255, null=True)),
78
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
79
+            ],
80
+            options={
81
+                'db_table': 'bel_master_view',
82
+                'managed': False,
83
+            },
84
+        ),
85
+        migrations.CreateModel(
86
+            name='EMasterView',
87
+            fields=[
88
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
89
+                ('PRO1', models.CharField(max_length=255, null=True)),
90
+                ('PRO1C', models.CharField(max_length=255, null=True)),
91
+                ('PRO2', models.CharField(max_length=255, null=True)),
92
+                ('PRO5', models.CharField(max_length=255, null=True)),
93
+                ('PRO8', models.CharField(max_length=255, null=True)),
94
+                ('PRO9', models.CharField(max_length=255, null=True)),
95
+                ('PRO10', models.CharField(max_length=255, null=True)),
96
+                ('PRO11', models.CharField(max_length=255, null=True)),
97
+                ('PRO12', models.CharField(max_length=255, null=True)),
98
+                ('PRO13', models.CharField(max_length=255, null=True)),
99
+                ('PRO14', models.CharField(max_length=255, null=True)),
100
+                ('PRO15', models.CharField(max_length=255, null=True)),
101
+                ('PRO16', models.CharField(max_length=255, null=True)),
102
+                ('PRO17', models.CharField(max_length=255, null=True)),
103
+                ('PRO18', models.CharField(max_length=255, null=True)),
104
+                ('PRO21', models.CharField(max_length=255, null=True)),
105
+                ('PRO25', models.CharField(max_length=255, null=True)),
106
+                ('PRO27', models.CharField(max_length=255, null=True)),
107
+                ('P2', models.CharField(max_length=255, null=True)),
108
+                ('PRO6', models.CharField(max_length=255, null=True)),
109
+                ('SPEED', models.CharField(max_length=255, null=True)),
110
+                ('PRO4', models.CharField(max_length=255, null=True)),
111
+                ('MC11', models.CharField(max_length=255, null=True)),
112
+                ('MC12', models.CharField(max_length=255, null=True)),
113
+                ('MC14', models.CharField(max_length=255, null=True)),
114
+                ('MC15', models.CharField(max_length=255, null=True)),
115
+                ('MC16', models.CharField(max_length=255, null=True)),
116
+                ('MC19', models.CharField(max_length=255, null=True)),
117
+                ('MC20', models.CharField(max_length=255, null=True)),
118
+                ('MC21', models.CharField(max_length=255, null=True)),
119
+                ('MC22', models.CharField(max_length=255, null=True)),
120
+                ('MC23', models.CharField(max_length=255, null=True)),
121
+                ('MC24', models.CharField(max_length=255, null=True)),
122
+                ('MP45', models.CharField(max_length=255, null=True)),
123
+                ('MP49', models.CharField(max_length=255, null=True)),
124
+                ('MI13', models.CharField(max_length=255, null=True)),
125
+                ('TC', models.CharField(max_length=255, null=True)),
126
+                ('MI14', models.CharField(max_length=255, null=True)),
127
+                ('MI15', models.CharField(max_length=255, null=True)),
128
+                ('MI16', models.CharField(max_length=255, null=True)),
129
+                ('MI17', models.CharField(max_length=255, null=True)),
130
+                ('MI18', models.CharField(max_length=255, null=True)),
131
+                ('MI19', models.CharField(max_length=255, null=True)),
132
+                ('MI20', models.CharField(max_length=255, null=True)),
133
+                ('MI21', models.CharField(max_length=255, null=True)),
134
+                ('MI22', models.CharField(max_length=255, null=True)),
135
+                ('MI23', models.CharField(max_length=255, null=True)),
136
+                ('MI31', models.CharField(max_length=255, null=True)),
137
+                ('MI33', models.CharField(max_length=255, null=True)),
138
+                ('INSAGM', models.CharField(max_length=255, null=True)),
139
+                ('MARAGM', models.CharField(max_length=255, null=True)),
140
+                ('MI53', models.CharField(max_length=255, null=True)),
141
+                ('MI55', models.CharField(max_length=255, null=True)),
142
+                ('MI36', models.CharField(max_length=255, null=True)),
143
+                ('MI39', models.CharField(max_length=255, null=True)),
144
+                ('MI24', models.CharField(max_length=255, null=True)),
145
+                ('Ind1', models.CharField(max_length=255, null=True)),
146
+                ('Ind2', models.CharField(max_length=255, null=True)),
147
+                ('Ind3', models.CharField(max_length=255, null=True)),
148
+                ('AGR1', models.CharField(max_length=255, null=True)),
149
+                ('AGR2', models.CharField(max_length=255, null=True)),
150
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
151
+            ],
152
+            options={
153
+                'db_table': 'e_master_view',
154
+                'managed': False,
155
+            },
156
+        ),
157
+        migrations.CreateModel(
158
+            name='MgMasterView',
159
+            fields=[
160
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
161
+                ('PRO1', models.CharField(max_length=255, null=True)),
162
+                ('PRO1C', models.CharField(max_length=255, null=True)),
163
+                ('PRO2', models.CharField(max_length=255, null=True)),
164
+                ('PRO5', models.CharField(max_length=255, null=True)),
165
+                ('PRO8', models.CharField(max_length=255, null=True)),
166
+                ('PRO9', models.CharField(max_length=255, null=True)),
167
+                ('PRO10', models.CharField(max_length=255, null=True)),
168
+                ('PRO11', models.CharField(max_length=255, null=True)),
169
+                ('PRO12', models.CharField(max_length=255, null=True)),
170
+                ('PRO13', models.CharField(max_length=255, null=True)),
171
+                ('PRO14', models.CharField(max_length=255, null=True)),
172
+                ('PRO15', models.CharField(max_length=255, null=True)),
173
+                ('PRO16', models.CharField(max_length=255, null=True)),
174
+                ('PRO17', models.CharField(max_length=255, null=True)),
175
+                ('PRO18', models.CharField(max_length=255, null=True)),
176
+                ('PRO21', models.CharField(max_length=255, null=True)),
177
+                ('PRO25', models.CharField(max_length=255, null=True)),
178
+                ('PRO27', models.CharField(max_length=255, null=True)),
179
+                ('P2', models.CharField(max_length=255, null=True)),
180
+                ('SPEED', models.CharField(max_length=255, null=True)),
181
+                ('PRO4', models.CharField(max_length=255, null=True)),
182
+                ('MC11', models.CharField(max_length=255, null=True)),
183
+                ('MC12', models.CharField(max_length=255, null=True)),
184
+                ('MC14', models.CharField(max_length=255, null=True)),
185
+                ('MC15', models.CharField(max_length=255, null=True)),
186
+                ('MC16', models.CharField(max_length=255, null=True)),
187
+                ('MC19', models.CharField(max_length=255, null=True)),
188
+                ('MC20', models.CharField(max_length=255, null=True)),
189
+                ('MC21', models.CharField(max_length=255, null=True)),
190
+                ('MC22', models.CharField(max_length=255, null=True)),
191
+                ('MC23', models.CharField(max_length=255, null=True)),
192
+                ('MC24', models.CharField(max_length=255, null=True)),
193
+                ('MP45', models.CharField(max_length=255, null=True)),
194
+                ('MP49', models.CharField(max_length=255, null=True)),
195
+                ('MI13', models.CharField(max_length=255, null=True)),
196
+                ('TC', models.CharField(max_length=255, null=True)),
197
+                ('MI14', models.CharField(max_length=255, null=True)),
198
+                ('MI15', models.CharField(max_length=255, null=True)),
199
+                ('MI16', models.CharField(max_length=255, null=True)),
200
+                ('MI17', models.CharField(max_length=255, null=True)),
201
+                ('MI18', models.CharField(max_length=255, null=True)),
202
+                ('MI19', models.CharField(max_length=255, null=True)),
203
+                ('MI20', models.CharField(max_length=255, null=True)),
204
+                ('MI21', models.CharField(max_length=255, null=True)),
205
+                ('MI22', models.CharField(max_length=255, null=True)),
206
+                ('MI23', models.CharField(max_length=255, null=True)),
207
+                ('MI31', models.CharField(max_length=255, null=True)),
208
+                ('MI33', models.CharField(max_length=255, null=True)),
209
+                ('INSAGM', models.CharField(max_length=255, null=True)),
210
+                ('MARAGM', models.CharField(max_length=255, null=True)),
211
+                ('MI53', models.CharField(max_length=255, null=True)),
212
+                ('MI55', models.CharField(max_length=255, null=True)),
213
+                ('MI36', models.CharField(max_length=255, null=True)),
214
+                ('MI39', models.CharField(max_length=255, null=True)),
215
+                ('MI24', models.CharField(max_length=255, null=True)),
216
+                ('Ind1', models.CharField(max_length=255, null=True)),
217
+                ('Ind2', models.CharField(max_length=255, null=True)),
218
+                ('Ind3', models.CharField(max_length=255, null=True)),
219
+                ('AGR1', models.CharField(max_length=255, null=True)),
220
+                ('AGR2', models.CharField(max_length=255, null=True)),
221
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
222
+            ],
223
+            options={
224
+                'db_table': 'mg_master_view',
225
+                'managed': False,
226
+            },
227
+        ),
228
+        migrations.CreateModel(
229
+            name='VMasterView',
230
+            fields=[
231
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
232
+                ('PRO1', models.CharField(max_length=255, null=True)),
233
+                ('PRO1C', models.CharField(max_length=255, null=True)),
234
+                ('PRO2', models.CharField(max_length=255, null=True)),
235
+                ('PRO5', models.CharField(max_length=255, null=True)),
236
+                ('PRO8', models.CharField(max_length=255, null=True)),
237
+                ('PRO9', models.CharField(max_length=255, null=True)),
238
+                ('PRO10', models.CharField(max_length=255, null=True)),
239
+                ('PRO11', models.CharField(max_length=255, null=True)),
240
+                ('PRO12', models.CharField(max_length=255, null=True)),
241
+                ('PRO13', models.CharField(max_length=255, null=True)),
242
+                ('PRO14', models.CharField(max_length=255, null=True)),
243
+                ('PRO15', models.CharField(max_length=255, null=True)),
244
+                ('PRO16', models.CharField(max_length=255, null=True)),
245
+                ('PRO17', models.CharField(max_length=255, null=True)),
246
+                ('PRO18', models.CharField(max_length=255, null=True)),
247
+                ('PRO21', models.CharField(max_length=255, null=True)),
248
+                ('PRO25', models.CharField(max_length=255, null=True)),
249
+                ('PRO27', models.CharField(max_length=255, null=True)),
250
+                ('P2', models.CharField(max_length=255, null=True)),
251
+                ('PRO6', models.CharField(max_length=255, null=True)),
252
+                ('SPEED', models.CharField(max_length=255, null=True)),
253
+                ('PRO4', models.CharField(max_length=255, null=True)),
254
+                ('MC11', models.CharField(max_length=255, null=True)),
255
+                ('MC12', models.CharField(max_length=255, null=True)),
256
+                ('MC14', models.CharField(max_length=255, null=True)),
257
+                ('MC15', models.CharField(max_length=255, null=True)),
258
+                ('MC16', models.CharField(max_length=255, null=True)),
259
+                ('MC19', models.CharField(max_length=255, null=True)),
260
+                ('MC20', models.CharField(max_length=255, null=True)),
261
+                ('MC21', models.CharField(max_length=255, null=True)),
262
+                ('MC22', models.CharField(max_length=255, null=True)),
263
+                ('MC23', models.CharField(max_length=255, null=True)),
264
+                ('MC24', models.CharField(max_length=255, null=True)),
265
+                ('MP34', models.CharField(max_length=255, null=True)),
266
+                ('MP39', models.CharField(max_length=255, null=True)),
267
+                ('MI13', models.CharField(max_length=255, null=True)),
268
+                ('TC', models.CharField(max_length=255, null=True)),
269
+                ('MI14', models.CharField(max_length=255, null=True)),
270
+                ('MI15', models.CharField(max_length=255, null=True)),
271
+                ('MI16', models.CharField(max_length=255, null=True)),
272
+                ('MI17', models.CharField(max_length=255, null=True)),
273
+                ('MI18', models.CharField(max_length=255, null=True)),
274
+                ('MI19', models.CharField(max_length=255, null=True)),
275
+                ('MI20', models.CharField(max_length=255, null=True)),
276
+                ('MI21', models.CharField(max_length=255, null=True)),
277
+                ('MI22', models.CharField(max_length=255, null=True)),
278
+                ('MI23', models.CharField(max_length=255, null=True)),
279
+                ('MI31', models.CharField(max_length=255, null=True)),
280
+                ('MI33', models.CharField(max_length=255, null=True)),
281
+                ('INSAGM', models.CharField(max_length=255, null=True)),
282
+                ('MARAGM', models.CharField(max_length=255, null=True)),
283
+                ('MI53', models.CharField(max_length=255, null=True)),
284
+                ('MI55', models.CharField(max_length=255, null=True)),
285
+                ('MI36', models.CharField(max_length=255, null=True)),
286
+                ('MI39', models.CharField(max_length=255, null=True)),
287
+                ('MI24', models.CharField(max_length=255, null=True)),
288
+                ('Ind1', models.CharField(max_length=255, null=True)),
289
+                ('Ind2', models.CharField(max_length=255, null=True)),
290
+                ('Ind3', models.CharField(max_length=255, null=True)),
291
+                ('AGR1', models.CharField(max_length=255, null=True)),
292
+                ('AGR2', models.CharField(max_length=255, null=True)),
293
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
294
+            ],
295
+            options={
296
+                'db_table': 'v_master_view',
297
+                'managed': False,
298
+            },
299
+        ),
300
+    ]

+ 2 - 2
app/legacy/templates/legacy/datacrud_list.html

@@ -30,7 +30,7 @@
30 30
     <thead>
31 31
         <tr class="bg-gray-100 text-left text-sm uppercase">
32 32
             {% for field in fields %}
33
-                <th class="border border-gray-200 px-4 py-2 text-left">{{ field.verbose_name }}</th>
33
+            <th class="border border-gray-200 px-4 py-2 text-left">{% firstof field.verbose_name field.name %} </th>
34 34
             {% endfor %}
35 35
             <th class="py-2 px-4 border-b">Actions</th>
36 36
         </tr>
@@ -47,7 +47,7 @@
47 47
                     {% elif field.name == 'file' and obj.file %}
48 48
                     <a href="{{ obj.file.url }}" target="_blank">View</a>
49 49
                     {% else %}
50
-                        {{ obj|attr:field.name }}
50
+                        {{ obj|attr:field.name | safe_floatformat:2 }}
51 51
                     {% endif %}
52 52
                 </td>
53 53
                 {% endfor %}

+ 10 - 0
app/legacy/templatetags/legacy_filters.py

@@ -1,5 +1,6 @@
1 1
 from django import template
2 2
 from django.template import Context
3
+from django.template.defaultfilters import floatformat
3 4
 
4 5
 
5 6
 register = template.Library()
@@ -27,3 +28,12 @@ def render_breadcrumbs(context, breadcrumbs):
27 28
     Context({
28 29
         'breadcrumbs': breadcrumbs
29 30
     }))
31
+
32
+@register.filter
33
+def safe_floatformat(value, decimal_places):
34
+    try:
35
+        # Attempt to convert the value to a float and format it
36
+        return floatformat(float(value), decimal_places)
37
+    except (ValueError, TypeError):
38
+        # If the value is not a number, return it as is
39
+        return value

+ 26 - 2
app/legacy/urls.py

@@ -1,6 +1,7 @@
1 1
 from django.urls import path
2 2
 from .views import DataListView, DataDetailView, DataCreateView, DataUpdateView, DataDeleteView,\
3
-DataMsCRUDView, TbFgPressInfoLotListCRUDView, LotSummaryCRUDView, VMasterViewCRUDView, MgMasterViewCRUDView, BelMasterViewCRUDView, EMasterViewCRUDView
3
+DataMsCRUDView, TbFgPressInfoLotListCRUDView, LotSummaryCRUDView, VMasterViewCRUDView, MgMasterViewCRUDView,\
4
+BelMasterViewCRUDView, EMasterViewCRUDView, DataRLCRUDView, DataWbCRUDView, LotSummaryRlCRUDView, LotSummaryWbCRUDView
4 5
 
5 6
 app_name = 'legacy'  # Namespace for this app
6 7
 
@@ -11,7 +12,10 @@ vm_crud = VMasterViewCRUDView()
11 12
 mg_crud = MgMasterViewCRUDView()
12 13
 bel_crud = BelMasterViewCRUDView()
13 14
 em_crud = EMasterViewCRUDView()
14
-
15
+datarl_crud = DataRLCRUDView()
16
+datawb_crud = DataWbCRUDView()
17
+lsrl_crud = LotSummaryRlCRUDView()
18
+lswb_crud = LotSummaryWbCRUDView()
15 19
 
16 20
 urlpatterns = [
17 21
     path('data/', DataListView.as_view(), name='data-list'),            # data/
@@ -24,6 +28,16 @@ urlpatterns = [
24 28
     path('datams/create/', datams_crud.get_create_view().as_view(), name='datams-create'),
25 29
     path('datams/<int:pk>/update/', datams_crud.get_update_view().as_view(), name='datams-update'),
26 30
     path('datams/<int:pk>/delete/', datams_crud.get_delete_view().as_view(), name='datams-delete'),
31
+
32
+    path('datarl/', datarl_crud.get_list_view().as_view(), name='datarl-list'),
33
+    path('datarl/create/', datarl_crud.get_create_view().as_view(), name='datarl-create'),
34
+    path('datarl/<int:pk>/update/', datarl_crud.get_update_view().as_view(), name='datarl-update'),
35
+    path('datarl/<int:pk>/delete/', datarl_crud.get_delete_view().as_view(), name='datarl-delete'),
36
+
37
+    path('datawb/', datawb_crud.get_list_view().as_view(), name='datawb-list'),
38
+    path('datawb/create/', datawb_crud.get_create_view().as_view(), name='datawb-create'),
39
+    path('datawb/<int:pk>/update/', datawb_crud.get_update_view().as_view(), name='datawb-update'),
40
+    path('datawb/<int:pk>/delete/', datawb_crud.get_delete_view().as_view(), name='datawb-delete'),
27 41
     
28 42
     path('fg/', fg_crud.get_list_view().as_view(), name='fg-list'),
29 43
     path('fg/create/', fg_crud.get_create_view().as_view(), name='fg-create'),
@@ -34,6 +48,7 @@ urlpatterns = [
34 48
     path('ls/create/', ls_crud.get_create_view().as_view(), name='ls-create'),
35 49
     path('ls/<int:pk>/update/', ls_crud.get_update_view().as_view(), name='ls-update'),
36 50
     path('ls/<int:pk>/delete/', ls_crud.get_delete_view().as_view(), name='ls-delete'),
51
+
37 52
     path('vm/', vm_crud.get_list_view().as_view(), name='vm-list'),
38 53
     path('vm/create/', vm_crud.get_create_view().as_view(), name='vm-create'),
39 54
     path('vm/<str:pk>/update/', vm_crud.get_update_view().as_view(), name='vm-update'),
@@ -55,4 +70,13 @@ urlpatterns = [
55 70
     path('em/<str:pk>/delete/', em_crud.get_delete_view().as_view(), name='em-delete'),
56 71
 
57 72
 
73
+    path('lsrl/', lsrl_crud.get_list_view().as_view(), name='lsrl-list'),
74
+    path('lsrl/create/', lsrl_crud.get_create_view().as_view(), name='lsrl-create'),
75
+    path('lsrl/<int:pk>/update/', lsrl_crud.get_update_view().as_view(), name='lsrl-update'),
76
+    path('lsrl/<int:pk>/delete/', lsrl_crud.get_delete_view().as_view(), name='lsrl-delete'),
77
+
78
+    path('lswb/', lswb_crud.get_list_view().as_view(), name='lswb-list'),
79
+    path('lswb/create/', lswb_crud.get_create_view().as_view(), name='lswb-create'),
80
+    path('lswb/<int:pk>/update/', lswb_crud.get_update_view().as_view(), name='lswb-update'),
81
+    path('lswb/<int:pk>/delete/', lswb_crud.get_delete_view().as_view(), name='lswb-delete'),
58 82
 ]

+ 75 - 3
app/legacy/views.py

@@ -11,8 +11,9 @@ from django.views.generic import (
11 11
     DeleteView,
12 12
 )
13 13
 from django.core.paginator import Paginator
14
-from .models import Data, DataMs, TbFgPressinfoLotlist, LotSummary
15
-from .filters import DataFilter, DataMsFilter, TbFgPressFilter, LotSummaryFilter
14
+from .models import Data, DataMs, TbFgPressinfoLotlist, LotSummary, DataRl, DataWb, LotSummaryRl, LotSummaryWb
15
+from .filters import DataFilter, DataMsFilter, TbFgPressFilter, LotSummaryFilter, \
16
+        DataRlFilter, DataWbFilter, LotSummaryRlFilter, LotSummaryWbFilter
16 17
 from django.urls import reverse
17 18
 from django.contrib import messages
18 19
 from pprint import pprint
@@ -158,7 +159,7 @@ class LotSummaryCRUDView(ConfigurableCRUDView):
158 159
     detail_template_name = 'legacy/datacrud_detail.html'
159 160
     form_template_name = 'legacy/datacrud_form.html'
160 161
     confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
161
-    filterset_class = DataMsFilter
162
+    filterset_class = LotSummaryFilter
162 163
 
163 164
     page_title = "Lot Summary"
164 165
 
@@ -301,3 +302,74 @@ class EMasterViewCRUDView(ConfigurableCRUDView):
301 302
     # Default ordering
302 303
     # ordering = ["-id", "PRO2"]
303 304
     
305
+class DataRLCRUDView(ConfigurableCRUDView):
306
+    model = DataRl
307
+    list_template_name = 'legacy/datacrud_list.html'
308
+    detail_template_name = 'legacy/datacrud_detail.html'
309
+    form_template_name = 'legacy/datacrud_form.html'
310
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
311
+    filterset_class = DataRlFilter
312
+
313
+    page_title = "Data RL"
314
+
315
+    # URL name mappings
316
+    list_url_name = 'legacy:datarl-list'
317
+    create_url_name = 'legacy:datarl-create'
318
+    update_url_name = 'legacy:datarl-update'
319
+    delete_url_name = 'legacy:datarl-delete'
320
+    # excludes = ["splitdata"]
321
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
322
+
323
+class DataWbCRUDView(ConfigurableCRUDView):
324
+    model = DataWb
325
+    list_template_name = 'legacy/datacrud_list.html'
326
+    detail_template_name = 'legacy/datacrud_detail.html'
327
+    form_template_name = 'legacy/datacrud_form.html'
328
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
329
+    filterset_class = DataWbFilter
330
+
331
+    page_title = "Data WB"
332
+
333
+    # URL name mappings
334
+    list_url_name = 'legacy:datawb-list'
335
+    create_url_name = 'legacy:datawb-create'
336
+    update_url_name = 'legacy:datawb-update'
337
+    delete_url_name = 'legacy:datawb-delete'
338
+    # excludes = ["splitdata"]
339
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
340
+
341
+class LotSummaryRlCRUDView(ConfigurableCRUDView):
342
+    model = LotSummaryRl
343
+    list_template_name = 'legacy/datacrud_list.html'
344
+    detail_template_name = 'legacy/datacrud_detail.html'
345
+    form_template_name = 'legacy/datacrud_form.html'
346
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
347
+    filterset_class = LotSummaryRlFilter
348
+
349
+    page_title = "Lot Summary RL"
350
+
351
+    # URL name mappings
352
+    list_url_name = 'legacy:lsrl-list'
353
+    create_url_name = 'legacy:lsrl-create'
354
+    update_url_name = 'legacy:lsrl-update'
355
+    delete_url_name = 'legacy:lsrl-delete'
356
+    # excludes = ["splitdata"]
357
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
358
+
359
+class LotSummaryWbCRUDView(ConfigurableCRUDView):
360
+    model = LotSummaryWb
361
+    list_template_name = 'legacy/datacrud_list.html'
362
+    detail_template_name = 'legacy/datacrud_detail.html'
363
+    form_template_name = 'legacy/datacrud_form.html'
364
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
365
+    filterset_class = LotSummaryWbFilter
366
+
367
+    page_title = "Lot Summary WB"
368
+
369
+    # URL name mappings
370
+    list_url_name = 'legacy:lswb-list'
371
+    create_url_name = 'legacy:lswb-create'
372
+    update_url_name = 'legacy:lswb-update'
373
+    delete_url_name = 'legacy:lswb-delete'
374
+    # excludes = ["splitdata"]
375
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first

+ 1 - 0
app/package.json

@@ -1,6 +1,7 @@
1 1
 {
2 2
   "dependencies": {
3 3
     "alpinejs": "^3.14.7",
4
+    "axios": "^1.7.9",
4 5
     "font-awesome": "^4.7.0",
5 6
     "heroicons": "^2.2.0",
6 7
     "tailwindcss": "^3.4.17",

BIN
app/report/checkbox_checked.jpg


BIN
app/report/checkbox_unchecked.jpg


BIN
app/report/coi_templates.xlsx


+ 280 - 0
app/report/gen_report.py

@@ -0,0 +1,280 @@
1
+
2
+from openpyxl import load_workbook, Workbook
3
+from datetime import datetime
4
+from openpyxl.drawing.image import Image
5
+import re
6
+
7
+from openpyxl.drawing.spreadsheet_drawing import AbsoluteAnchor
8
+from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
9
+from openpyxl.utils.units import pixels_to_EMU
10
+from openpyxl.utils import get_column_letter, column_index_from_string
11
+from django.db import models
12
+import os
13
+from django.db.models.fields.files import ImageFieldFile
14
+from pprint import pprint
15
+from PIL import Image as PILImage
16
+from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
17
+from openpyxl.utils.units import pixels_to_EMU
18
+from openpyxl.drawing.spreadsheet_drawing import AnchorMarker, TwoCellAnchor
19
+
20
+
21
+def set_image_with_offset_old(sheet, img, cell_coordinate, offset_x=0, offset_y=0):
22
+    """
23
+    Add an image to the sheet with an offset relative to the top-left corner of a cell.
24
+
25
+    :param sheet: The worksheet
26
+    :param img: The openpyxl Image object
27
+    :param cell_coordinate: Cell to place the image (e.g., "B2")
28
+    :param offset_x: Horizontal offset in pixels
29
+    :param offset_y: Vertical offset in pixels
30
+    """
31
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
32
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
33
+
34
+    # Get column width and row height in pixels
35
+    col_width = sheet.column_dimensions[get_column_letter(column_index_from_string(col_letter))].width or 10
36
+    row_height = sheet.row_dimensions[row_number].height or 15
37
+
38
+    # Approximate conversion of Excel units to pixels
39
+    col_pixels = col_width * 7.5  # Excel's ~7.5 pixels per width unit
40
+    row_pixels = row_height * 0.75  # Approximation for row height in pixels
41
+
42
+    # Calculate absolute positions based on offsets
43
+    anchor_x = col_pixels + offset_x
44
+    anchor_y = row_pixels + offset_y
45
+
46
+    # Set the anchor for the image
47
+    img.anchor = cell_coordinate
48
+    img.anchor.dx = int(anchor_x * 9525)  # Convert to EMUs (Excel Measurement Units)
49
+    img.anchor.dy = int(anchor_y * 9525)  # Convert to EMUs (Excel Measurement Units)
50
+
51
+    sheet.add_image(img)
52
+
53
+
54
+def set_image_with_offset(sheet, img, cell_coordinate, offset_x=0, offset_y=0):
55
+    """
56
+    Add an image to the sheet with an offset relative to the top-left corner of a cell.
57
+
58
+    :param sheet: The worksheet
59
+    :param img: The openpyxl Image object
60
+    :param cell_coordinate: Cell to place the image (e.g., "B2")
61
+    :param offset_x: Horizontal offset in pixels
62
+    :param offset_y: Vertical offset in pixels
63
+    """
64
+    # Extract the column and row from the cell coordinate
65
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
66
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
67
+
68
+    # Get the zero-based indices for the cell
69
+    col_idx = column_index_from_string(col_letter) - 1
70
+    row_idx = row_number - 1
71
+
72
+    # Approximate column width and row height to pixels
73
+    col_width = sheet.column_dimensions[col_letter].width or 10  # Default column width
74
+    row_height = sheet.row_dimensions[row_number].height or 15  # Default row height
75
+
76
+    # Convert column width and row height to pixels
77
+    col_pixels = col_width * 7.5  # Approximation: ~7.5 pixels per width unit
78
+    row_pixels = row_height * 0.75  # Approximation: ~0.75 pixels per height unit
79
+
80
+    # Calculate the position in pixels for the top-left corner of the cell
81
+    cell_x = col_idx * col_pixels
82
+    cell_y = row_idx * row_pixels
83
+
84
+    # Apply the offsets
85
+    final_x = cell_x + offset_x
86
+    final_y = cell_y + offset_y
87
+
88
+    # Convert to EMUs
89
+    pos = XDRPoint2D(pixels_to_EMU(final_x), pixels_to_EMU(final_y))
90
+    size = XDRPositiveSize2D(pixels_to_EMU(img.width), pixels_to_EMU(img.height))
91
+
92
+    # Set the image's anchor with the position and size
93
+    img.anchor = AbsoluteAnchor(pos=pos, ext=size)
94
+
95
+    # Add the image to the worksheet
96
+    sheet.add_image(img)
97
+
98
+
99
+
100
+def center_image_in_cell(sheet, img, cell_coordinate):
101
+    """
102
+    Center an image inside a specified cell.
103
+
104
+    :param sheet: The worksheet
105
+    :param img: The openpyxl Image object
106
+    :param cell_coordinate: The cell to center the image in (e.g., "C3")
107
+    """
108
+    # Extract column and row from the cell coordinate
109
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
110
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
111
+    col_idx = column_index_from_string(col_letter) - 1  # Convert to zero-based column index
112
+
113
+    # Get cell dimensions
114
+    col_width = sheet.column_dimensions[col_letter].width or 10  # Default width if not set
115
+    row_height = sheet.row_dimensions[row_number].height or 15  # Default height if not set
116
+
117
+    # Convert dimensions to pixels (approximation)
118
+    col_pixels = col_width * 7.5  # 1 Excel column width unit ≈ 7.5 pixels
119
+    row_pixels = row_height * 0.75  # 1 Excel row height unit ≈ 0.75 pixels
120
+
121
+    # Get image dimensions
122
+    img_width, img_height = img.width, img.height
123
+
124
+    # Calculate offsets to center the image
125
+    offset_x = int((col_pixels - img_width) / 2 * pixels_to_EMU(1))  # Center horizontally
126
+    offset_y = int((row_pixels - img_height) / 2 * pixels_to_EMU(1))  # Center vertically
127
+
128
+    # Define the anchor for the image
129
+    _from = AnchorMarker(col=col_idx, row=row_number - 1, colOff=offset_x, rowOff=offset_y)
130
+    to = AnchorMarker(col=col_idx + 1, row=row_number, colOff=-offset_x, rowOff=-offset_y)
131
+
132
+    # Use TwoCellAnchor for positioning
133
+    img.anchor = TwoCellAnchor(editAs="oneCell", _from=_from, to=to)
134
+
135
+    # Add the image to the sheet
136
+    sheet.add_image(img)
137
+
138
+
139
+def gen_xlsx(template_file, selected_sheets, prefix_filename, data):
140
+    """
141
+    Generate an Excel file from a template, fill placeholders, and include only selected sheets.
142
+
143
+    Args:
144
+        template_file (str): Path to the Excel template file.
145
+        selected_sheets (list): List of sheet names to include in the output file.
146
+        prefix_filename (str): Prefix for the output filename.
147
+        data (dict): Data dictionary with sheet-specific keys and fallback keys.
148
+
149
+    Returns:
150
+        str: Path of the generated Excel file.
151
+    """
152
+    checked_image_path = "/app/report/checkbox_checked.jpg"  # Path to the checked checkbox image
153
+    unchecked_image_path = "/app/report/checkbox_unchecked.jpg"  # Path to the unchecked checkbox image
154
+
155
+    # Load the template workbook
156
+    workbook = load_workbook(template_file)
157
+
158
+    # Remove sheets not in selected_sheets
159
+    for sheet_name in workbook.sheetnames:
160
+        if sheet_name not in selected_sheets:
161
+            del workbook[sheet_name]
162
+
163
+    # Process the selected sheets
164
+    for sheet_name in selected_sheets:
165
+        if sheet_name not in workbook.sheetnames:
166
+            raise ValueError(f"Sheet '{sheet_name}' not found in the template.")
167
+
168
+        sheet = workbook[sheet_name]
169
+
170
+        # Replace placeholders with actual values
171
+        # Handle hiding rows based on patterns in data
172
+         
173
+        for row in sheet.iter_rows():
174
+            for cell in row:
175
+                if cell.value and isinstance(cell.value, str) and cell.value.startswith("<") and cell.value.endswith(">"):
176
+                    placeholder = cell.value.strip("<>")
177
+                    
178
+                    # Determine value priority: `sheet_name.key` > `key`
179
+                    value = None
180
+                    sheet_specific_key = f"{sheet_name}.{placeholder}"
181
+                    if sheet_specific_key in data:
182
+                        value = data[sheet_specific_key]
183
+                    elif placeholder in data:
184
+                        value = data[placeholder]
185
+                        
186
+                    if value is not None:
187
+                        if isinstance(value, ImageFieldFile):
188
+                            pprint("ImageField")
189
+                            image_path = value.path
190
+                            if os.path.exists(image_path):
191
+                                # img = Image(image_path)
192
+                                # img.height = 40  # Adjust size as needed
193
+                                pil_img = PILImage.open(image_path)
194
+                                original_width, original_height = pil_img.size
195
+
196
+                                # Desired height (e.g., 40), calculate the new width to maintain aspect ratio
197
+                                desired_height = 40
198
+                                aspect_ratio = original_width / original_height
199
+                                new_width = int(desired_height * aspect_ratio)
200
+
201
+                                # Resize the image using Pillow (optional, for saving memory during export)
202
+                                resized_img = pil_img.resize((new_width, desired_height), PILImage.Resampling.LANCZOS)
203
+                                resized_img.save(image_path)  # Save the resized image back to the same path
204
+
205
+                                # Insert the resized image into the Excel sheet
206
+                                img = Image(image_path)
207
+                                img.width, img.height = new_width, desired_height  # Set the dimensions
208
+                                # sheet.add_image(img, cell.coordinate)
209
+                                center_image_in_cell(sheet, img, cell.coordinate, )
210
+                                cell.value = None  # Clear placeholder
211
+
212
+                        elif value is True:
213
+                            img = Image(checked_image_path)
214
+                            img.width = img.height = 10
215
+                            print(f"{cell.coordinate}")
216
+                            # sheet.add_image(img, cell.coordinate)
217
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
218
+                            center_image_in_cell(sheet,img, cell.coordinate, )
219
+                            cell.value = None  # Remove the placeholder text
220
+                        elif value is False:
221
+                            img = Image(unchecked_image_path)
222
+                            img.width = img.height = 10
223
+                            # sheet.add_image(img, cell.coordinate)
224
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
225
+                            center_image_in_cell(sheet, img, cell.coordinate, )
226
+                            cell.value = None  # Remove the placeholder text
227
+                        else:
228
+                            # Insert the text value directly
229
+                            cell.value = value
230
+
231
+        for key, value in data.items():
232
+            if isinstance(value, str) and re.match(r"^\d+\[\d+:\d+\]$", value):
233
+                # Parse the prefix and row range
234
+                prefix, row_range = value.split("[")
235
+                row_start, row_end = map(int, row_range[:-1].split(":"))
236
+                
237
+                # Hide rows if the prefix matches the condition
238
+                if prefix == "0":  # Adjust the condition as needed
239
+                    sheet.row_dimensions.group(row_start, row_end, hidden=True)
240
+        
241
+
242
+    # Generate the output filename with a timestamp
243
+    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
244
+    output_path = f"{prefix_filename}_{timestamp}.xlsx"
245
+    workbook.save(output_path)
246
+
247
+    return output_path
248
+
249
+if __name__ == "__main__":
250
+    # Example usage
251
+    data = {
252
+        "customer": "Tum Coder",
253
+        "inspect_date": "2025-01-15",
254
+        "lot_no": "12345",
255
+        "staff_name":  "Tum 8888",
256
+        "man_name":  "Tum 999",
257
+        "size": "Large",
258
+        "pcs": "10 pcs",
259
+        "spec": "Spec-A",
260
+        "hardness.d1_act": "10",
261
+        "hardness.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
262
+        "hardness.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
263
+        "hardness.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
264
+        "dimension_app.d1_act": "33",
265
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
266
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
267
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
268
+    }
269
+
270
+
271
+
272
+
273
+    output_file = gen_xlsx(
274
+        template_file="./hardness.xlsx",
275
+        selected_sheets=["hardness", "dimension_app"],  # Replace with your actual sheet names
276
+        prefix_filename="./output/output",
277
+        data=data
278
+    )
279
+
280
+    print(f"Generated file: {output_file}")

+ 98 - 45
app/report/templates/report/coi.html

@@ -3,66 +3,63 @@
3 3
 {% block title %}Report Dashboard{% endblock %}
4 4
 
5 5
 {% block content %}
6
-<div class="container mx-auto px-4 py-8">
6
+<div class="container mx-auto px-4 py-8" x-data="COIReport">
7 7
 
8 8
   <h1 class="text-2xl font-bold text-gray-800">Export Center</h1>
9 9
   <form method='post'>
10 10
     {% csrf_token %}
11
-    <div class="flex items-center justify-between mb-4">
12
-      <h1 class="text-lg font-bold text-gray-800">TKX Certificate Issue</h1>
13
-      <button class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
14
-        รายละเอียด CheckedBy|ApproveBy
15
-      </button>
16
-    </div>
17 11
     <div class="flex items-center gap-2 mb-4">
18 12
       <label for="lot-number" class="text-gray-700 font-medium">Lot No. :</label>
19
-      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required>
13
+      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required x-model="lot_no">
20 14
       <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type="submit" name="search_lot">
21 15
         🔍
22 16
       </button>
23 17
     </div>
24
-    <div class="flex items-center gap-4 mb-4">
25 18
       <button class="bg-blue-100 text-blue-700 px-4 py-2 rounded hover:bg-blue-200">
26 19
         Option for Export :
27 20
       </button>
28
-      <div class="flex items-center space-x-2">
29
-        <label class="flex items-center space-x-1">
30
-          <input type="checkbox" class="rounded" name='exports' value='size_dimension'>
31
-          <span>Size Dimension</span>
32
-        </label>
33
-        <label class="flex items-center space-x-1">
34
-          <input type="checkbox" class="rounded" name='exports' value='grade'>
35
-          <span>Grade</span>
36
-        </label>
21
+        <div class="grid grid-cols-4 gap-4">
22
+        {% for key,value in SHEET_NAMES.items %}
37 23
         <label class="flex items-center space-x-1">
38
-          <input type="checkbox" class="rounded" name='exports' value='wb'>
39
-          <span>WB</span>
24
+          <input type="checkbox" class="rounded" name='exports' value='{{ key }}' x-model='exports'>
25
+          <span>{{ value }}</span>
40 26
         </label>
41
-        <label class="flex items-center space-x-1">
42
-          <input type="checkbox" class="rounded" name='exports' value='fg_weight'>
43
-          <span>FG Weight</span>
44
-        </label>
45
-        <label class="flex items-center space-x-1">
46
-          <input type="checkbox" class="rounded" name='exports' value='centering'>
47
-          <span>Centering</span>
48
-        </label>
49
-        <label class="flex items-center space-x-1">
50
-          <input type="checkbox" class="rounded" name='exports' value='ring_test'>
51
-          <span>Ring Test</span>
52
-        </label>
53
-        <label class="flex items-center space-x-1">
54
-          <input type="checkbox" class="rounded" name='exports' value='rotate'>
55
-          <span>Rotate</span>
56
-        </label>
57
-        <label class="flex items-center space-x-1">
58
-          <input type="checkbox" class="rounded" name='exports' value='final_judge'>
59
-          <span>Final Judge</span>
60
-        </label>
61
-      </div>
62
-      <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type='submit' name='export'>
63
-        Export Excel
64
-      </button>
65
-    </div>
27
+        {% endfor %}
28
+        </div>
29
+        <div class="grid grid-cols-2 gap-4">
30
+          <div class="my-4">
31
+            <label for="qa1" class="block mb-2 text-sm font-medium text-gray-900">Select QA.1</label>
32
+            <select id="qa1" name="qa1" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa1'>
33
+              <option value="" disabled selected>Choose a user</option>
34
+              {% for user in users %}
35
+              <option value="{{ user.id }}">{{ user.profile }}</option>
36
+              {% endfor %}
37
+            </select>
38
+          </div>
39
+          <div class="my-4">
40
+            <label for="qa2" class="block mb-2 text-sm font-medium text-gray-900">Select QA.2</label>
41
+            <select id="qa2" name="qa2" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa2'>
42
+              <option value="" disabled selected>Choose a user</option>
43
+              {% for user in users %}
44
+              <option value="{{ user.id }}">{{ user.profile }}</option>
45
+              {% endfor %}
46
+            </select>
47
+          </div>
48
+        </div>
49
+        <div class="flex justify-end my-3 space-x-4">
50
+          <div>
51
+            <button  type='button' class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" 
52
+                                   @click="exportCOI">
53
+              Export Excel
54
+            </button>
55
+          </div>
56
+          <!-- TODO: add download here -->
57
+          <div class=" text-center" x-show="downloadUrl">
58
+            <a :href="downloadUrl"  class="block px-4 py-2 bg-green-600 text-white  rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" download>
59
+              Download Report
60
+            </a>
61
+          </div>
62
+        </div>
66 63
   </form>
67 64
   {{ result }}
68 65
   
@@ -130,3 +127,59 @@
130 127
 <div class="border-t border-gray-300 my-4"></div>
131 128
 {% include "report/_cen.html" %}
132 129
 {% endblock %}
130
+{% block footer_script %}
131
+<script type="text/javascript">
132
+  function COIReport() {
133
+    return {
134
+      lot_no: '', // Bind this to the input value
135
+      exports: [],
136
+      qa1: null,
137
+      qa2: null,
138
+      gen_report_url: '{% url "report:gen_report" %}', 
139
+      downloadUrl: null, // Stores the download link after export success
140
+      init() {
141
+        //alert("COI Report");
142
+        this.qa1 = '';
143
+        this.qa2 = '';
144
+      },
145
+      async exportCOI() {
146
+              if (!this.lot_no) {
147
+                  alert("Please enter a Lot No.");
148
+                  return;
149
+              }
150
+
151
+              try {
152
+                  // Make POST request using Axios
153
+                  const response = await axios.post(this.gen_report_url, {
154
+                      lot_no: this.lot_no,
155
+                      exports: this.exports,
156
+                      qa1: this.qa1,
157
+                      qa2: this.qa2
158
+                  });
159
+
160
+                  if (response.status === 200) {
161
+                      const result = response.data;
162
+                      alert(`Report generated successfully: ${result.file_url}`);
163
+                      console.log('File URL:', result.file_url);
164
+                      this.downloadUrl = result.file_url; // Set the download URL
165
+                  }
166
+              } catch (error) {
167
+                  if (error.response) {
168
+                      // Server responded with a status other than 2xx
169
+                      alert(`Error: ${error.response.data.message || 'Failed to generate report'}`);
170
+                      console.error('Error response:', error.response);
171
+                  } else if (error.request) {
172
+                      // Request was made but no response received
173
+                      alert('No response from server.');
174
+                      console.error('Request error:', error.request);
175
+                  } else {
176
+                      // Something else caused the error
177
+                      alert('An unexpected error occurred.');
178
+                      console.error('Unexpected error:', error.message);
179
+                  }
180
+              }
181
+          },
182
+    };
183
+  }
184
+</script>
185
+{% endblock %}

+ 2 - 0
app/report/urls.py

@@ -14,6 +14,8 @@ urlpatterns = [
14 14
     path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
15 15
     path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
16 16
     path('coi/', views.coi_view, name='coi-view'),
17
+    path('report/generate/', views.gen_report_view, name='gen_report'),
18
+
17 19
     # path('create/', views.create_report, name='create'),  # Create a new report
18 20
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
19 21
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 223 - 3
app/report/views.py

@@ -8,6 +8,16 @@ from .filters import ReportFilter
8 8
 from .forms import ExportOptionsForm
9 9
 from pprint import pprint
10 10
 
11
+from .gen_report import gen_xlsx
12
+from django.core.files.base import File
13
+from pathlib import Path
14
+from django.views.decorators.csrf import csrf_exempt
15
+from django.http import JsonResponse, HttpResponseBadRequest
16
+import json
17
+from django.contrib.auth.decorators import login_required
18
+from django.contrib.auth.models import User
19
+from legacy.models import Data
20
+
11 21
 
12 22
 def index(request):
13 23
     reports = Report.objects.all()
@@ -58,9 +68,183 @@ class ReportCRUDView(ConfigurableCRUDView):
58 68
     # config_edit_fields = ["lot_no", "code"]
59 69
     ordering = ["-created_at", "-id",]
60 70
 
71
+def convert_sheet_data(sheet_data):
72
+    """
73
+    Convert sheet_data to the required form with prefixed keys.
74
+    
75
+    :param sheet_data: Dictionary with sheet names as keys and their data as values.
76
+    :return: Dictionary in the required key-value format.
77
+    """
78
+    converted_data = {}
79
+
80
+    for sheet_name, data in sheet_data.items():
81
+        for key, value in data.items():
82
+            # Prefix each key with the sheet name
83
+            converted_key = f"{sheet_name}.{key}"
84
+            converted_data[converted_key] = value
85
+
86
+    return converted_data
87
+
88
+def generate_hardness_out_values(lot_no):
89
+    """
90
+    Generate a dictionary of placeholder values for a given lot_no.
91
+
92
+    :param lot_no: The lot number to query data for.
93
+    :return: A dictionary with placeholders (e.g., v1_1, v1_2, ...) as keys and their respective values.
94
+    """
95
+    # Query the Data model for records matching the given lot_no
96
+    records = Data.objects.filter(lot_no=lot_no).order_by('row_no')
97
+    print(f"records {lot_no} = {records.values()}")
98
+    # Initialize an empty dictionary to store placeholder values
99
+    placeholders = {}
100
+
101
+    # Iterate over the records to populate placeholder values
102
+    for record_idx, record in enumerate(records, start=1):
103
+        placeholders[f'v{record_idx}_1'] = record.p1  # Checkpoint 1 value
104
+        placeholders[f'v{record_idx}_2'] = record.p2  # Checkpoint 2 value
105
+        placeholders[f'v{record_idx}_3'] = record.p3  # Checkpoint 3 value
106
+        placeholders[f'v{record_idx}_4'] = record.avg  # Average value
107
+        placeholders[f'v{record_idx}_5'] = record.rgrade  # Judgment value
108
+
109
+    return placeholders
110
+
111
+def merge_sheet_data_with_data(sheet_data, data):
112
+    """
113
+    Merge `sheet_data` with `data`.
114
+
115
+    :param sheet_data: Dictionary containing the sheet-specific data.
116
+    :param data: Dictionary containing general data.
117
+    :return: A merged dictionary combining both `sheet_data` and `data`.
118
+    """
119
+    # Merge dictionaries using unpacking
120
+    merged_data = {**data, **sheet_data}
121
+
122
+    return merged_data
123
+
124
+def create_coi_file(lot_no, sheets, user, md):
125
+    pprint("---- create_coi_file ---")
126
+    pprint(md)
127
+    qa1 = User.objects.get(pk=md['qa1'])
128
+    qa2 = User.objects.get(pk=md['qa2'])
129
+
130
+    pprint(qa1)
131
+    pprint(qa2)
132
+    
133
+    sheet_data = {}
134
+    for sheet_name in sheets:
135
+        if sheet_name == 'hardness_out':
136
+            sheet_data[sheet_name] = generate_hardness_out_values(lot_no)
137
+    converted_data = convert_sheet_data(sheet_data)
138
+
139
+    print(f"sheet_data \n {sheet_data}") 
140
+    print(f"converted_data \n {converted_data}")
141
+
142
+    data = {
143
+        "customer": "Tum Coder",
144
+        "inspect_date": "2025-01-15",
145
+        "lot_no": "12345",
146
+        "staff_name":  "Tum 8888",
147
+        "man_name":  "Tum 999",
148
+        "size": "Large",
149
+        "pcs": "10 pcs",
150
+        "spec": "Spec-A",
151
+        "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
+        "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
153
+        "acc": True,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
155
+        # "hardness_out.qa1": f"{qa1.first_name} {qa1.last_name}",
156
+        # "hardness_out.qa2": f"{qa2.first_name} {qa2.last_name}",
157
+        "qa1": f"{qa1.first_name} {qa1.last_name}",
158
+        "qa2": f"{qa2.first_name} {qa2.last_name}",
159
+        "dimension_app.d1_act": "33",
160
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
161
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
162
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
163
+        "sign1": qa1.profile.signed_picture,
164
+        "sign2": qa2.profile.signed_picture,
165
+        "pos1": qa1.profile.get_position_display(),
166
+        "pos2": qa2.profile.get_position_display()
167
+    }
168
+    merged_data = merge_sheet_data_with_data(converted_data, data)
169
+    pprint(f"---- merged_data ---")
170
+    pprint(merged_data)
171
+
172
+    output_file = gen_xlsx(
173
+        template_file="/app/report/coi_templates.xlsx",
174
+        selected_sheets=sheets,  # Replace with your actual sheet names
175
+        prefix_filename="/app/media/coi",
176
+        data=merged_data
177
+    )
178
+    report = Report.objects.create(
179
+        name=lot_no,
180
+        created_by=user,
181
+        file=None  # Leave this as None or assign a file if required
182
+    )
183
+    output_file_path = Path(output_file)  # Convert to a Path object for convenience
184
+    with open(output_file_path, "rb") as f:
185
+        report.file.save(output_file_path.name, File(f), save=True)
186
+
187
+    pprint(f"outputfile = {output_file}")
188
+    return report
189
+
190
+SHEET_NAMES = {
191
+    'hardness_out': 'Hardness Out',
192
+    'hardness_out_in': 'Hardness Out/In', 
193
+    'hardness_both_size': 'Hardness Both Size',
194
+    'dimension': 'Dimension',
195
+    'dimension_app': 'Dimension Appearance',
196
+    'dimension_bal_weight': 'Dimension Balance/Weight',
197
+    'dim_bal_app_hard': 'Dimension Balance/Appearance/Hardness',
198
+    'dim_bal_app_rot_hard': 'Dimension Balance/Appearance/Rotation/Hardness',
199
+    'thickness_8_point': 'Thickness 8 Points',
200
+    'centering': 'Centering',
201
+}
61 202
 def coi_view(request):
62 203
     pprint(f"xxxx method = xxx {request.method}")
204
+    users = User.objects.all()
205
+
63 206
     if request.method == "POST":
207
+        pprint(request.POST)
208
+        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
209
+        pprint(f"Selected Export Options: {exports}")
210
+        
211
+        if 'export' in request.POST:
212
+
213
+            data = {
214
+                "customer": "Tum Coder",
215
+                "inspect_date": "2025-01-15",
216
+                "lot_no": "12345",
217
+                "staff_name":  "Tum 8888",
218
+                "man_name":  "Tum 999",
219
+                "size": "Large",
220
+                "lot_size": "10 pcs",
221
+                "spec": "Spec-A",
222
+                "hardness_out.d1_act": "10",
223
+                "hardness_out.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
224
+                "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
225
+                "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
226
+                "dimension_app.d1_act": "33",
227
+                "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
228
+                "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
229
+                "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
230
+            }
231
+            output_file = gen_xlsx(
232
+                template_file="/app/report/coi_templates.xlsx",
233
+                selected_sheets=exports,  # Replace with your actual sheet names
234
+                prefix_filename="/app/media/coi",
235
+                data=data
236
+            )
237
+            report = Report.objects.create(
238
+                name=request.POST.get('lot_no','Untitled'),
239
+                created_by=request.user,
240
+                file=None  # Leave this as None or assign a file if required
241
+            )
242
+            output_file_path = Path(output_file)  # Convert to a Path object for convenience
243
+            with open(output_file_path, "rb") as f:
244
+                report.file.save(output_file_path.name, File(f), save=True)
245
+
246
+            pprint(f"outputfile = {output_file}")
247
+
64 248
         if 'search_lot' in request.POST:
65 249
             lot_no = request.POST.get('lot_no', None)
66 250
             lot_no = lot_no.strip()
@@ -78,8 +262,44 @@ def coi_view(request):
78 262
                                                            'size_str': size_str,
79 263
                                                            'spec': spec})
80 264
 
81
-        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
82
-        pprint(f"Selected Export Options: {exports}")
83 265
         messages.success(request, "Request Sent")
84 266
         return redirect(request.path_info)
85
-    return render(request, 'report/coi.html')
267
+    return render(request, 'report/coi.html', {'SHEET_NAMES': SHEET_NAMES, 'users': users})
268
+
269
+
270
+@csrf_exempt  # Disable CSRF for API requests (ensure this is secure in production)
271
+@login_required
272
+def gen_report_view(request):
273
+    if request.method == "POST":
274
+        try:
275
+            # Parse JSON data from the request body
276
+            data = json.loads(request.body)
277
+            lot_no = data.get("lot_no").strip()
278
+            exports = data.get("exports")
279
+            qa1 = data.get('qa1')
280
+            qa2 = data.get('qa2')
281
+            print(f"data = {data}")
282
+
283
+            if not lot_no:
284
+                return HttpResponseBadRequest("Missing 'lot_no' in request data")
285
+
286
+            # Call the `create_coi_file` function with the provided lot_no
287
+            report = create_coi_file(lot_no, exports, request.user, {'qa1': qa1, 'qa2': qa2})
288
+
289
+            # Return a success response with the report details
290
+            return JsonResponse({
291
+                "message": "Report generated successfully",
292
+                "report_id": report.id,
293
+                "file_url": report.file.url if report.file else None,
294
+            })
295
+
296
+        except json.JSONDecodeError:
297
+            return HttpResponseBadRequest("Invalid JSON data")
298
+        except Exception as e:
299
+            pprint(e)
300
+            return JsonResponse({"error": str(e)}, status=500)
301
+    else:
302
+        return HttpResponseBadRequest("Only POST requests are allowed")
303
+
304
+
305
+

~$hardness.xlsx → app/report/~$coi_templates.xlsx


+ 3 - 0
app/sysadmin/apps.py

@@ -4,3 +4,6 @@ from django.apps import AppConfig
4 4
 class SysadminConfig(AppConfig):
5 5
     default_auto_field = 'django.db.models.BigAutoField'
6 6
     name = 'sysadmin'
7
+
8
+    def ready(self):
9
+        import sysadmin.signals

+ 12 - 0
app/sysadmin/filters.py

@@ -0,0 +1,12 @@
1
+
2
+import django_filters
3
+from django.contrib.auth.models import User
4
+
5
+class UserFilter(django_filters.FilterSet):
6
+    username = django_filters.CharFilter(field_name='username', lookup_expr='icontains')
7
+    first_name = django_filters.CharFilter(field_name='first_name', lookup_expr='icontains')
8
+    last_name = django_filters.CharFilter(field_name='last_name', lookup_expr='icontains')
9
+
10
+    class Meta:
11
+        model = User
12
+        fields = ['username', 'first_name', 'last_name']  # Add fields you want to filter

+ 61 - 2
app/sysadmin/forms.py

@@ -2,6 +2,11 @@
2 2
 from django import forms
3 3
 from django.contrib.auth.forms import AuthenticationForm
4 4
 
5
+from django.contrib.auth.forms import UserCreationForm
6
+from django.contrib.auth.models import User
7
+
8
+from .models import UserProfile
9
+
5 10
 class CustomLoginForm(AuthenticationForm):
6 11
     username = forms.CharField(widget=forms.TextInput(attrs={
7 12
         'placeholder': 'Username'
@@ -11,8 +16,6 @@ class CustomLoginForm(AuthenticationForm):
11 16
     }))
12 17
 
13 18
 # forms.py
14
-from django.contrib.auth.forms import UserCreationForm
15
-from django.contrib.auth.models import User
16 19
 
17 20
 class CustomUserCreationForm(UserCreationForm):
18 21
     class Meta:
@@ -33,3 +36,59 @@ class CustomUserCreationForm(UserCreationForm):
33 36
         self.fields['password2'].widget.attrs.update({
34 37
             'placeholder': 'Confirm Password'
35 38
         })
39
+
40
+
41
+class UserProfileForm(forms.ModelForm):
42
+    class Meta:
43
+        model = UserProfile
44
+        fields = ['profile_picture', 'position', 'signed_picture']  # Include the fields you want to manage
45
+
46
+class UserCustomForm(forms.ModelForm):
47
+    # Profile fields
48
+    profile_picture = forms.ImageField(required=False, label="Profile Picture")
49
+    signed_picture = forms.ImageField(required=False, label="Signed Picture")
50
+    position = forms.ChoiceField(
51
+        choices=UserProfile.POSITION_CHOICES, 
52
+        required=False, 
53
+        label="Position"
54
+    )
55
+
56
+    class Meta:
57
+        model = User
58
+        fields = [
59
+            "first_name",
60
+            "last_name",
61
+            "is_staff",
62
+            "is_superuser",
63
+            "is_active",
64
+        ]
65
+
66
+    def __init__(self, *args, **kwargs):
67
+        # Allow passing `instance` to access both User and UserProfile objects
68
+        user_instance = kwargs.pop('instance', None)
69
+        profile_instance = getattr(user_instance, 'profile', None)
70
+        super().__init__(instance=user_instance, *args, **kwargs)
71
+
72
+        # Populate initial data for profile fields if `profile` exists
73
+        if profile_instance:
74
+            self.fields['profile_picture'].initial = profile_instance.profile_picture
75
+            self.fields['signed_picture'].initial = profile_instance.signed_picture
76
+            self.fields['position'].initial = profile_instance.position
77
+
78
+    def save(self, commit=True):
79
+        # Save the User instance first
80
+        user = super().save(commit=commit)
81
+        profile_data = {
82
+            'profile_picture': self.cleaned_data.get('profile_picture'),
83
+            'signed_picture': self.cleaned_data.get('signed_picture'),
84
+            'position': self.cleaned_data.get('position'),
85
+        }
86
+
87
+        # Ensure profile exists for the user
88
+        profile, created = UserProfile.objects.get_or_create(user=user)
89
+        for key, value in profile_data.items():
90
+            setattr(profile, key, value)
91
+        if commit:
92
+            profile.save()
93
+
94
+        return user

+ 29 - 0
app/sysadmin/migrations/0001_initial.py

@@ -0,0 +1,29 @@
1
+# Generated by Django 4.2 on 2025-01-17 04:58
2
+
3
+from django.conf import settings
4
+from django.db import migrations, models
5
+import django.db.models.deletion
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='UserProfile',
19
+            fields=[
20
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('bio', models.TextField(blank=True, null=True)),
22
+                ('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile/%Y/%m/%d/')),
23
+                ('signed_picture', models.ImageField(blank=True, null=True, upload_to='signed/%Y/%m/%d/')),
24
+                ('email', models.EmailField(blank=True, max_length=254, null=True)),
25
+                ('position', models.CharField(blank=True, choices=[('QA_STAFF', 'QA Staff'), ('QA_MANAGER', 'QA Manager')], max_length=20, null=True)),
26
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
27
+            ],
28
+        ),
29
+    ]

+ 20 - 0
app/sysadmin/models.py

@@ -1,3 +1,23 @@
1 1
 from django.db import models
2 2
 
3 3
 # Create your models here.
4
+
5
+from django.contrib.auth.models import User
6
+
7
+class UserProfile(models.Model):
8
+    POSITION_CHOICES = [
9
+        ('QA_STAFF', 'QA Staff'),
10
+        ('QA_MANAGER', 'QA. MG.'),
11
+        ('QA_AST_MANAGER', 'QA. Asst. MG.'),
12
+        ('QA_ENGINEER', 'QA. Engineer'),
13
+    ]
14
+    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
15
+    bio = models.TextField(blank=True, null=True)
16
+    profile_picture = models.ImageField(upload_to="profile/%Y/%m/%d/", blank=True, null=True)
17
+    signed_picture = models.ImageField(upload_to="signed/%Y/%m/%d/", blank=True, null=True)
18
+    email = models.EmailField(blank=True, null=True)  # New email field
19
+    position = models.CharField(max_length=20, choices=POSITION_CHOICES, blank=True, null=True)  # New position field
20
+
21
+    def __str__(self):
22
+        pos = self.get_position_display()
23
+        return f"{self.user.username} / {self.user.first_name} {self.user.last_name} #{pos}"

+ 13 - 0
app/sysadmin/signals.py

@@ -0,0 +1,13 @@
1
+from django.db.models.signals import post_save
2
+from django.dispatch import receiver
3
+from django.contrib.auth.models import User
4
+from .models import UserProfile
5
+
6
+@receiver(post_save, sender=User)
7
+def create_user_profile(sender, instance, created, **kwargs):
8
+    if created:
9
+        UserProfile.objects.create(user=instance)
10
+
11
+@receiver(post_save, sender=User)
12
+def save_user_profile(sender, instance, **kwargs):
13
+    instance.profile.save()

+ 94 - 0
app/sysadmin/templates/sysadmin/profile.html

@@ -0,0 +1,94 @@
1
+{% extends "base.html" %}
2
+{% load tailwind_filters %}
3
+
4
+{% block title %}Report Dashboard{% endblock %}
5
+{% block content %}
6
+<div class="container mx-auto px-4 py-8">
7
+<h1 class="text-2xl font-bold text-gray-700 mb-6">Your Profile</h1>
8
+
9
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
10
+            <!-- Username -->
11
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
12
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
13
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
14
+  <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
15
+</svg>
16
+
17
+                </div>
18
+                <div class="ml-4">
19
+                    <h3 class="text-lg font-medium text-gray-900">Username</h3>
20
+                    <p class="text-gray-600">{{ user.username }}</p>
21
+                </div>
22
+            </div>
23
+
24
+            <!-- Email -->
25
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
26
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
27
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
28
+  <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
29
+</svg>
30
+
31
+                </div>
32
+                <div class="ml-4">
33
+                    <h3 class="text-lg font-medium text-gray-900">Email</h3>
34
+                    <p class="text-gray-600">{{ user.email }}</p>
35
+                </div>
36
+            </div>
37
+        </div>
38
+<form method="POST" enctype="multipart/form-data" class='space-y-4'>
39
+    {% csrf_token %}
40
+              <!-- First Name -->
41
+              <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
42
+
43
+                <div>
44
+                  <label for="first_name" class="block text-sm font-medium text-gray-700">First Name</label>
45
+                  <input
46
+                      type="text"
47
+                      name="first_name"
48
+                      id="first_name"
49
+                      value="{{ user.first_name }}"
50
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
51
+                      />
52
+                </div>
53
+
54
+                <!-- Last Name -->
55
+                <div>
56
+                  <label for="last_name" class="block text-sm font-medium text-gray-700">Last Name</label>
57
+                  <input
58
+                      type="text"
59
+                      name="last_name"
60
+                      id="last_name"
61
+                      value="{{ user.last_name }}"
62
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
63
+                      />
64
+                </div>
65
+              </div>
66
+    {% if form.instance.profile_picture %}
67
+    <!-- Show uploaded profile picture -->
68
+    <img src="{{ form.instance.profile_picture.url }}" 
69
+         alt="Profile Picture" 
70
+         class="w-32 h-32 rounded-full mb-4 shadow-md">
71
+    {% else %}
72
+    <!-- Placeholder image if no profile picture -->
73
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
74
+      <span>No Image</span>
75
+    </div>
76
+    {% endif %}
77
+    {{ form.profile_picture }}
78
+    {% if form.instance.signed_picture %}
79
+    <!-- Show uploaded profile picture -->
80
+    <img src="{{ form.instance.signed_picture.url }}" 
81
+         alt="Profile Picture" 
82
+         class="w-32 mb-4 shadow-md">
83
+    {% else %}
84
+    <!-- Placeholder image if no profile picture -->
85
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
86
+      <span>No Image</span>
87
+    </div>
88
+    {% endif %}
89
+    {{ form.signed_picture }}
90
+    {{ form.position | as_crispy_field }}
91
+    <button type="submit" class="px-6 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Save Changes</button>
92
+</form>
93
+</div>
94
+{% endblock %}

+ 71 - 0
app/sysadmin/templates/sysadmin/user_form.html

@@ -0,0 +1,71 @@
1
+{% extends "base.html" %}
2
+
3
+{% load legacy_filters %}
4
+{% load tailwind_filters %}
5
+{% load crispy_forms_filters %}
6
+
7
+{% block title %}
8
+    {% if view.title %}
9
+        {{ view.title }}
10
+    {% else %}
11
+        {{ view|class_name }}
12
+    {% endif %}
13
+{% endblock %}
14
+
15
+{% block content %}
16
+<div class="container mx-auto px-4 py-6">
17
+    <h1 class="text-2xl font-bold mb-6">
18
+        {% if view.title %}
19
+            {{ view.title }}
20
+        {% elif view|class_name == "CreateViewClass" %}
21
+            Create {{ model_verbose_name }}
22
+        {% else %}
23
+            Update {{ model_verbose_name }}
24
+        {% endif %}
25
+    </h1>
26
+
27
+    <!-- Render the Form -->
28
+    <form method="post" enctype="multipart/form-data">
29
+        {% csrf_token %}
30
+        <div class="flex flex-wrap items-center space-x-4">
31
+            <!-- Show profile_picture -->
32
+            <div class=" mb-4">
33
+                {% if form.instance.profile and form.instance.profile.profile_picture %}
34
+                    <div class="mb-2">
35
+                        <img src="{{ form.instance.profile.profile_picture.url }}" alt="Profile Picture" class="max-w-xs border rounded" width=100>
36
+                    </div>
37
+                {% endif %}
38
+                {{ form.profile_picture|as_crispy_field }}
39
+            </div>
40
+
41
+            <!-- Show signed_picture -->
42
+            <div class=" mb-4">
43
+                {% if form.instance.profile and form.instance.profile.signed_picture %}
44
+                    <div class="mb-2">
45
+                        <img src="{{ form.instance.profile.signed_picture.url }}" alt="Signed Picture" class="max-w-xs border rounded" width=100>
46
+                    </div>
47
+                {% endif %}
48
+                {{ form.signed_picture|as_crispy_field }}
49
+            </div>
50
+
51
+            <!-- Render other form fields -->
52
+            {% for field in form %}
53
+                {% if field.name not in "profile_picture signed_picture" %}
54
+                    <div class=" mb-4">
55
+                        {{ field|as_crispy_field }}
56
+                    </div>
57
+                {% endif %}
58
+            {% endfor %}
59
+        </div>
60
+
61
+        <div class="mt-4">
62
+            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
63
+                Save
64
+            </button>
65
+            <a href="{% url list_url_name %}" class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400">
66
+                Cancel
67
+            </a>
68
+        </div>
69
+    </form>
70
+</div>
71
+{% endblock %}

+ 8 - 0
app/sysadmin/urls.py

@@ -3,8 +3,16 @@ from . import views
3 3
 
4 4
 app_name = "sysadmin"  # Use this namespace for reverse URL lookups
5 5
 
6
+users_crud = views.UserCRUDView()
7
+
6 8
 urlpatterns = [
7 9
     path('login/', views.login_view, name='login'),
8 10
     path('register/', views.register_view, name='register'),
9 11
     path('logout/', views.logout_view, name='logout'),
12
+    path('profile/', views.profile_view, name='profile'),  # Add profile view URL
13
+
14
+    path('users/', users_crud.get_list_view().as_view(), name='users-list'),
15
+    path('users/create/', users_crud.get_create_view().as_view(), name='users-create'),
16
+    path('users/<int:pk>/update/', users_crud.get_update_view().as_view(), name='users-update'),
17
+    path('users/<int:pk>/delete/', users_crud.get_delete_view().as_view(), name='users-delete'),
10 18
 ]

+ 57 - 1
app/sysadmin/views.py

@@ -1,7 +1,13 @@
1 1
 # views.py
2 2
 from django.contrib.auth import authenticate, login, logout
3 3
 from django.shortcuts import render, redirect
4
-from .forms import CustomLoginForm, CustomUserCreationForm
4
+from .forms import CustomLoginForm, CustomUserCreationForm, UserProfileForm, UserCustomForm
5
+from .models import UserProfile
6
+from .filters import UserFilter
7
+from django.contrib.auth.decorators import login_required
8
+from django.contrib import messages
9
+from core.utils import ConfigurableCRUDView
10
+from django.contrib.auth.models import User
5 11
 
6 12
 def login_view(request):
7 13
     if request.method == "POST":
@@ -30,3 +36,53 @@ def register_view(request):
30 36
 def logout_view(request):
31 37
     logout(request)  # Logs out the user
32 38
     return redirect('sysadmin:login')  # Redirect to the login page after logout
39
+
40
+@login_required
41
+def profile_view(request):
42
+    try:
43
+        # Get the profile for the current user
44
+        profile = request.user.profile
45
+    except UserProfile.DoesNotExist:
46
+        # Create a profile if it doesn't exist
47
+        profile = UserProfile.objects.create(user=request.user)
48
+
49
+    if request.method == "POST":
50
+        form = UserProfileForm(request.POST, request.FILES, instance=profile)
51
+
52
+        user = request.user
53
+        user.first_name = request.POST.get('first_name', user.first_name)
54
+        user.last_name = request.POST.get('last_name', user.last_name)
55
+
56
+        if form.is_valid():
57
+            form.save()
58
+            user.save()
59
+            messages.success(request, "Profile Updated")
60
+            return redirect('sysadmin:profile')  # Redirect to the profile page after saving
61
+        else:
62
+            messages.error(request, form.errors)
63
+    else:
64
+        form = UserProfileForm(instance=profile)
65
+
66
+    return render(request, 'sysadmin/profile.html', {'form': form})
67
+
68
+
69
+class UserCRUDView(ConfigurableCRUDView):
70
+    model = User
71
+    list_template_name = 'legacy/datacrud_list.html'
72
+    detail_template_name = 'legacy/datacrud_detail.html'
73
+    form_template_name = 'sysadmin/user_form.html'
74
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
75
+    filterset_class = UserFilter
76
+
77
+    page_title = "Users"
78
+
79
+    # URL name mappings
80
+    list_url_name = 'sysadmin:users-list'
81
+    create_url_name = 'sysadmin:users-create'
82
+    update_url_name = 'sysadmin:users-update'
83
+    delete_url_name = 'sysadmin:users-delete'
84
+    # excludes = ["splitdata"]
85
+    config_fields = ("id", "username", "is_active", "is_staff", "is_superuser", "last_login")
86
+    config_field_orders = ["id",]  # Display these fields first
87
+    form_class = UserCustomForm
88
+    config_edit_fields = None

+ 44 - 12
app/templates/base.html

@@ -7,7 +7,7 @@
7 7
 <head>
8 8
     <meta charset="UTF-8">
9 9
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
-    <title>{% block title %}My Django App{% endblock %}</title>
10
+    <title>{% block title %}COI System{% endblock %}</title>
11 11
 
12 12
     <!-- TailwindCSS -->
13 13
     <!--
@@ -16,6 +16,7 @@
16 16
     <link href="{% static "font-awesome/css/font-awesome.css" %}" rel="stylesheet" />
17 17
     <script type="text/javascript" defer src="{% static "alpinejs/dist/cdn.min.js" %}"></script>
18 18
     <link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
19
+    <script type="text/javascript"  src="{% static "axios/dist/axios.js" %}"></script>
19 20
     <script type="text/javascript"  src="{% static "js/main.js" %}"></script>
20 21
 </head>
21 22
 <body class="bg-gray-100 text-gray-800">
@@ -39,7 +40,7 @@
39 40
 
40 41
                 {% if user.is_authenticated %}
41 42
                     <!-- Logged-in User -->
42
-                    <span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span>
43
+                    <a href="{% url 'sysadmin:profile' %}"><span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span></a>
43 44
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44 45
                 {% else %}
45 46
                     <!-- Guest User -->
@@ -81,6 +82,15 @@
81 82
                             <a href="{% url "legacy:data-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data</a>
82 83
                           </li>
83 84
                           <li>
85
+                            <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data MS</a>
86
+                          </li>
87
+                          <li>
88
+                            <a href="{% url "legacy:datarl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data RL</a>
89
+                          </li>
90
+                          <li>
91
+                            <a href="{% url "legacy:datawb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data WB</a>
92
+                          </li>
93
+                          <li>
84 94
                             <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Measurement</a>
85 95
                           </li>
86 96
                           <li>
@@ -90,6 +100,12 @@
90 100
                             <a href="{% url "legacy:ls-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary</a>
91 101
                           </li>
92 102
                           <li>
103
+                            <a href="{% url "legacy:lsrl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary RL</a>
104
+                          </li>
105
+                          <li>
106
+                            <a href="{% url "legacy:lswb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary WB</a>
107
+                          </li>
108
+                          <li>
93 109
                             <a href="{% url "legacy:vm-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">VMaster</a>
94 110
                           </li>
95 111
                           <li>
@@ -101,12 +117,26 @@
101 117
                           <li>
102 118
                             <a href="{% url "legacy:em-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">EMaster</a>
103 119
                           </li>
104
-                          <li>
105
-                             <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Invoice</a>
106
-                          </li>
107 120
                     </ul>
108 121
                  </li>
109
-                <li><a href="/settings/" class="flex items-center p-2 text-gray-900 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"><span class="ml-3">Settings</span></a></li>
122
+                 <li>
123
+
124
+                   <button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="setting-sub" data-collapse-toggle="setting-sub">
125
+                     <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
126
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 6c0 1.657-3.134 3-7 3S5 7.657 5 6m14 0c0-1.657-3.134-3-7-3S5 4.343 5 6m14 0v6M5 6v6m0 0c0 1.657 3.134 3 7 3s7-1.343 7-3M5 12v6c0 1.657 3.134 3 7 3s7-1.343 7-3v-6"/>
127
+                     </svg>
128
+
129
+                     <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
130
+                     <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
131
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
132
+                     </svg>
133
+                   </button>
134
+                   <ul id="setting-sub" class="hidden py-2 space-y-2">
135
+                     <li>
136
+                       <a href="{% url "sysadmin:users-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Users</a>
137
+                     </li>
138
+                   </ul>
139
+                 </li>
110 140
             </ul>
111 141
         </div>
112 142
     </aside>
@@ -144,14 +174,16 @@
144 174
 
145 175
     {% django_browser_reload_script %}
146 176
     <script>
147
-    // Auto-hide the message after 5 seconds
148
-    setTimeout(() => {
177
+      // Auto-hide the message after 5 seconds
178
+      setTimeout(() => {
149 179
         const alert = document.getElementById('message-alert');
150 180
         if (alert) {
151
-            alert.style.opacity = '0'; // Fade out
152
-            setTimeout(() => alert.remove(), 500); // Remove after fade-out
181
+          alert.style.opacity = '0'; // Fade out
182
+          setTimeout(() => alert.remove(), 500); // Remove after fade-out
153 183
         }
154
-    }, 5000); // 5 seconds
155
-</script>
184
+      }, 5000); // 5 seconds
185
+    </script>
186
+    {% block footer_script %}
187
+    {% endblock %}
156 188
 </body>
157 189
 </html>

BIN
hardness.xlsx


+ 3 - 1
test_xlsx2.py

@@ -139,6 +139,8 @@ if __name__ == "__main__":
139 139
         "customer": "Tum Coder",
140 140
         "inspect_date": "2025-01-15",
141 141
         "lot_no": "12345",
142
+        "staff_name":  "Tum 8888",
143
+        "man_name":  "Tum 999",
142 144
         "size": "Large",
143 145
         "pcs": "10 pcs",
144 146
         "spec": "Spec-A",
@@ -149,7 +151,7 @@ if __name__ == "__main__":
149 151
         "dimension_app.d1_act": "33",
150 152
         "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
151 153
         "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
-        "dimension_app.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
153 155
     }
154 156
 
155 157
 

Merge branch 'master' of http://git2.simplico.net/tum/network_report_server · 991ef40235 - Gogs: Simplico Git Service
Procházet zdrojové kódy

Merge branch 'master' of http://git2.simplico.net/tum/network_report_server

tum před 1 rokem
rodič
revize
991ef40235
1 změnil soubory, kde provedl 0 přidání a 0 odebrání
  1. binární
      app/Output/excel_out_test_excel_formatter_update.xlsx

binární
app/Output/excel_out_test_excel_formatter_update.xlsx