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
 

tmt/tiger_frontend - Gogs: Simplico Git Service

Geen omschrijving

golf 77628cf8bf first comm 2 jaren geleden
..
LICENSE 77628cf8bf first comm 2 jaren geleden
README.md 77628cf8bf first comm 2 jaren geleden
index.js 77628cf8bf first comm 2 jaren geleden
package.json 77628cf8bf first comm 2 jaren geleden

README.md

to-regex-range Donate NPM version NPM monthly downloads NPM total downloads Linux Build Status

Pass two numbers, get a regex-compatible source string for matching ranges. Validated against more than 2.78 million test assertions.

Please consider following this project's author, Jon Schlinkert, and consider starring the project to show your :heart: and support.

Install

Install with npm:

$ npm install --save to-regex-range

What does this do?


This libary generates the source string to be passed to new RegExp() for matching a range of numbers.

Example

const toRegexRange = require('to-regex-range');
const regex = new RegExp(toRegexRange('15', '95'));

A string is returned so that you can do whatever you need with it before passing it to new RegExp() (like adding ^ or $ boundaries, defining flags, or combining it another string).


Why use this library?


Convenience

Creating regular expressions for matching numbers gets deceptively complicated pretty fast.

For example, let's say you need a validation regex for matching part of a user-id, postal code, social security number, tax id, etc:

  • regex for matching 1 => /1/ (easy enough)
  • regex for matching 1 through 5 => /[1-5]/ (not bad...)
  • regex for matching 1 or 5 => /(1|5)/ (still easy...)
  • regex for matching 1 through 50 => /([1-9]|[1-4][0-9]|50)/ (uh-oh...)
  • regex for matching 1 through 55 => /([1-9]|[1-4][0-9]|5[0-5])/ (no prob, I can do this...)
  • regex for matching 1 through 555 => /([1-9]|[1-9][0-9]|[1-4][0-9]{2}|5[0-4][0-9]|55[0-5])/ (maybe not...)
  • regex for matching 0001 through 5555 => /(0{3}[1-9]|0{2}[1-9][0-9]|0[1-9][0-9]{2}|[1-4][0-9]{3}|5[0-4][0-9]{2}|55[0-4][0-9]|555[0-5])/ (okay, I get the point!)

The numbers are contrived, but they're also really basic. In the real world you might need to generate a regex on-the-fly for validation.

Learn more

If you're interested in learning more about character classes and other regex features, I personally have always found regular-expressions.info to be pretty useful.

Heavily tested

As of April 07, 2019, this library runs >1m test assertions against generated regex-ranges to provide brute-force verification that results are correct.

Tests run in ~280ms on my MacBook Pro, 2.5 GHz Intel Core i7.

Optimized

Generated regular expressions are optimized:

  • duplicate sequences and character classes are reduced using quantifiers
  • smart enough to use ? conditionals when number(s) or range(s) can be positive or negative
  • uses fragment caching to avoid processing the same exact string more than once


Usage

Add this library to your javascript application with the following line of code

const toRegexRange = require('to-regex-range');

The main export is a function that takes two integers: the min value and max value (formatted as strings or numbers).

const source = toRegexRange('15', '95');
//=> 1[5-9]|[2-8][0-9]|9[0-5]

const regex = new RegExp(`^${source}$`);
console.log(regex.test('14')); //=> false
console.log(regex.test('50')); //=> true
console.log(regex.test('94')); //=> true
console.log(regex.test('96')); //=> false

Options

options.capture

Type: boolean

Deafault: undefined

Wrap the returned value in parentheses when there is more than one regex condition. Useful when you're dynamically generating ranges.

console.log(toRegexRange('-10', '10'));
//=> -[1-9]|-?10|[0-9]

console.log(toRegexRange('-10', '10', { capture: true }));
//=> (-[1-9]|-?10|[0-9])

options.shorthand

Type: boolean

Deafault: undefined

Use the regex shorthand for [0-9]:

console.log(toRegexRange('0', '999999'));
//=> [0-9]|[1-9][0-9]{1,5}

console.log(toRegexRange('0', '999999', { shorthand: true }));
//=> \d|[1-9]\d{1,5}

options.relaxZeros

Type: boolean

Default: true

This option relaxes matching for leading zeros when when ranges are zero-padded.

const source = toRegexRange('-0010', '0010');
const regex = new RegExp(`^${source}$`);
console.log(regex.test('-10')); //=> true
console.log(regex.test('-010')); //=> true
console.log(regex.test('-0010')); //=> true
console.log(regex.test('10')); //=> true
console.log(regex.test('010')); //=> true
console.log(regex.test('0010')); //=> true

When relaxZeros is false, matching is strict:

const source = toRegexRange('-0010', '0010', { relaxZeros: false });
const regex = new RegExp(`^${source}$`);
console.log(regex.test('-10')); //=> false
console.log(regex.test('-010')); //=> false
console.log(regex.test('-0010')); //=> true
console.log(regex.test('10')); //=> false
console.log(regex.test('010')); //=> false
console.log(regex.test('0010')); //=> true

Examples

Range Result Compile time
toRegexRange(-10, 10) -[1-9]\|-?10\|[0-9] 132μs
toRegexRange(-100, -10) -1[0-9]\|-[2-9][0-9]\|-100 50μs
toRegexRange(-100, 100) -[1-9]\|-?[1-9][0-9]\|-?100\|[0-9] 42μs
toRegexRange(001, 100) 0{0,2}[1-9]\|0?[1-9][0-9]\|100 109μs
toRegexRange(001, 555) 0{0,2}[1-9]\|0?[1-9][0-9]\|[1-4][0-9]{2}\|5[0-4][0-9]\|55[0-5] 51μs
toRegexRange(0010, 1000) 0{0,2}1[0-9]\|0{0,2}[2-9][0-9]\|0?[1-9][0-9]{2}\|1000 31μs
toRegexRange(1, 50) [1-9]\|[1-4][0-9]\|50 24μs
toRegexRange(1, 55) [1-9]\|[1-4][0-9]\|5[0-5] 23μs
toRegexRange(1, 555) [1-9]\|[1-9][0-9]\|[1-4][0-9]{2}\|5[0-4][0-9]\|55[0-5] 30μs
toRegexRange(1, 5555) [1-9]\|[1-9][0-9]{1,2}\|[1-4][0-9]{3}\|5[0-4][0-9]{2}\|55[0-4][0-9]\|555[0-5] 43μs
toRegexRange(111, 555) 11[1-9]\|1[2-9][0-9]\|[2-4][0-9]{2}\|5[0-4][0-9]\|55[0-5] 38μs
toRegexRange(29, 51) 29\|[34][0-9]\|5[01] 24μs
toRegexRange(31, 877) 3[1-9]\|[4-9][0-9]\|[1-7][0-9]{2}\|8[0-6][0-9]\|87[0-7] 32μs
toRegexRange(5, 5) 5 8μs
toRegexRange(5, 6) 5\|6 11μs
toRegexRange(1, 2) 1\|2 6μs
toRegexRange(1, 5) [1-5] 15μs
toRegexRange(1, 10) [1-9]\|10 22μs
toRegexRange(1, 100) [1-9]\|[1-9][0-9]\|100 25μs
toRegexRange(1, 1000) [1-9]\|[1-9][0-9]{1,2}\|1000 31μs
toRegexRange(1, 10000) [1-9]\|[1-9][0-9]{1,3}\|10000 34μs
toRegexRange(1, 100000) [1-9]\|[1-9][0-9]{1,4}\|100000 36μs
toRegexRange(1, 1000000) [1-9]\|[1-9][0-9]{1,5}\|1000000 42μs
toRegexRange(1, 10000000) [1-9]\|[1-9][0-9]{1,6}\|10000000 42μs

Heads up!

Order of arguments

When the min is larger than the max, values will be flipped to create a valid range:

toRegexRange('51', '29');

Is effectively flipped to:

toRegexRange('29', '51');
//=> 29|[3-4][0-9]|5[0-1]

Steps / increments

This library does not support steps (increments). A pr to add support would be welcome.

History

v2.0.0 - 2017-04-21

New features

Adds support for zero-padding!

v1.0.0

Optimizations

Repeating ranges are now grouped using quantifiers. rocessing time is roughly the same, but the generated regex is much smaller, which should result in faster matching.

Attribution

Inspired by the python library range-regex.

About

Contributing

Pull requests and stars are always welcome. For bugs and feature requests, please create an issue.

Running Tests

Running and reviewing unit tests is a great way to get familiarized with a library and its API. You can install dependencies and run tests with the following command:

$ npm install && npm test

Building docs

(This project's readme.md is generated by verb, please don't edit the readme directly. Any changes to the readme must be made in the .verb.md readme template.)

To generate the readme, run the following command:

$ npm install -g verbose/verb#dev verb-generate-readme && verb

Related projects

You might also be interested in these projects:

  • expand-range: Fast, bash-like range expansion. Expand a range of numbers or letters, uppercase or lowercase. Used… more | homepage
  • fill-range: Fill in a range of numbers or letters, optionally passing an increment or step to… more | homepage
  • micromatch: Glob matching for javascript/node.js. A drop-in replacement and faster alternative to minimatch and multimatch. | homepage
  • repeat-element: Create an array by repeating the given value n times. | homepage
  • repeat-string: Repeat the given string n times. Fastest implementation for repeating a string. | homepage

Contributors

| Commits | Contributor |
| --- | --- |
| 63 | jonschlinkert |
| 3 | doowb |
| 2 | realityking |

Author

Jon Schlinkert

Please consider supporting me on Patreon, or start your own Patreon page!

License

Copyright © 2019, Jon Schlinkert. Released under the MIT License.


This file was generated by verb-generate-readme, v0.8.0, on April 07, 2019.