+
56
+class LotSummaryRlFilter(django_filters.FilterSet):
57
+    lot_no = django_filters.CharFilter(field_name='lot_no', lookup_expr='icontains')
58
+    code = django_filters.CharFilter(field_name='code', lookup_expr='icontains')
59
+
60
+    class Meta:
61
+        model = LotSummaryRl
62
+        fields = ['lot_no', 'code']  # Add fields you want to filter
63
+
64
+
65
+class LotSummaryWbFilter(django_filters.FilterSet):
66
+    lot_no = django_filters.CharFilter(field_name='lot_no', lookup_expr='icontains')
67
+    code = django_filters.CharFilter(field_name='code', lookup_expr='icontains')
68
+
69
+    class Meta:
70
+        model = LotSummaryWb
71
+        fields = ['lot_no', 'code']  # Add fields you want to filter
72
+

+ 300 - 0
app/legacy/migrations/0002_belmasterview_emasterview_mgmasterview_vmasterview.py

1
+# Generated by Django 4.2 on 2025-01-17 04:58
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('legacy', '0001_initial'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.CreateModel(
14
+            name='BelMasterView',
15
+            fields=[
16
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
17
+                ('PRO1', models.CharField(max_length=255, null=True)),
18
+                ('PRO1C', models.CharField(max_length=255, null=True)),
19
+                ('PRO2', models.CharField(max_length=255, null=True)),
20
+                ('PRO5', models.CharField(max_length=255, null=True)),
21
+                ('PRO8', models.CharField(max_length=255, null=True)),
22
+                ('PRO9', models.CharField(max_length=255, null=True)),
23
+                ('PRO10', models.CharField(max_length=255, null=True)),
24
+                ('PRO11', models.CharField(max_length=255, null=True)),
25
+                ('PRO12', models.CharField(max_length=255, null=True)),
26
+                ('PRO13', models.CharField(max_length=255, null=True)),
27
+                ('PRO14', models.CharField(max_length=255, null=True)),
28
+                ('PRO15', models.CharField(max_length=255, null=True)),
29
+                ('PRO16', models.CharField(max_length=255, null=True)),
30
+                ('PRO17', models.CharField(max_length=255, null=True)),
31
+                ('PRO18', models.CharField(max_length=255, null=True)),
32
+                ('PRO21', models.CharField(max_length=255, null=True)),
33
+                ('PRO25', models.CharField(max_length=255, null=True)),
34
+                ('PRO27', models.CharField(max_length=255, null=True)),
35
+                ('P2', models.CharField(max_length=255, null=True)),
36
+                ('PRO6', models.CharField(max_length=255, null=True)),
37
+                ('SPEED', models.CharField(max_length=255, null=True)),
38
+                ('PRO4', models.CharField(max_length=255, null=True)),
39
+                ('MC11', models.CharField(max_length=255, null=True)),
40
+                ('MC12', models.CharField(max_length=255, null=True)),
41
+                ('MC14', models.CharField(max_length=255, null=True)),
42
+                ('MC15', models.CharField(max_length=255, null=True)),
43
+                ('MC16', models.CharField(max_length=255, null=True)),
44
+                ('MC19', models.CharField(max_length=255, null=True)),
45
+                ('MC20', models.CharField(max_length=255, null=True)),
46
+                ('MC21', models.CharField(max_length=255, null=True)),
47
+                ('MC22', models.CharField(max_length=255, null=True)),
48
+                ('MC23', models.CharField(max_length=255, null=True)),
49
+                ('MC24', models.CharField(max_length=255, null=True)),
50
+                ('MP45', models.CharField(max_length=255, null=True)),
51
+                ('MP49', models.CharField(max_length=255, null=True)),
52
+                ('MI13', models.CharField(max_length=255, null=True)),
53
+                ('TC', models.CharField(max_length=255, null=True)),
54
+                ('MI14', models.CharField(max_length=255, null=True)),
55
+                ('MI15', models.CharField(max_length=255, null=True)),
56
+                ('MI16', models.CharField(max_length=255, null=True)),
57
+                ('MI17', models.CharField(max_length=255, null=True)),
58
+                ('MI18', models.CharField(max_length=255, null=True)),
59
+                ('MI19', models.CharField(max_length=255, null=True)),
60
+                ('MI20', models.CharField(max_length=255, null=True)),
61
+                ('MI21', models.CharField(max_length=255, null=True)),
62
+                ('MI22', models.CharField(max_length=255, null=True)),
63
+                ('MI23', models.CharField(max_length=255, null=True)),
64
+                ('MI31', models.CharField(max_length=255, null=True)),
65
+                ('MI33', models.CharField(max_length=255, null=True)),
66
+                ('INSAGM', models.CharField(max_length=255, null=True)),
67
+                ('MARAGM', models.CharField(max_length=255, null=True)),
68
+                ('MI53', models.CharField(max_length=255, null=True)),
69
+                ('MI55', models.CharField(max_length=255, null=True)),
70
+                ('MI36', models.CharField(max_length=255, null=True)),
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
     <thead>
30
     <thead>
31
         <tr class="bg-gray-100 text-left text-sm uppercase">
31
         <tr class="bg-gray-100 text-left text-sm uppercase">
32
             {% for field in fields %}
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
             {% endfor %}
34
             {% endfor %}
35
             <th class="py-2 px-4 border-b">Actions</th>
35
             <th class="py-2 px-4 border-b">Actions</th>
36
         </tr>
36
         </tr>
47
                     {% elif field.name == 'file' and obj.file %}
47
                     {% elif field.name == 'file' and obj.file %}
48
                     <a href="{{ obj.file.url }}" target="_blank">View</a>
48
                     <a href="{{ obj.file.url }}" target="_blank">View</a>
49
                     {% else %}
49
                     {% else %}
50
-                        {{ obj|attr:field.name }}
50
+                        {{ obj|attr:field.name | safe_floatformat:2 }}
51
                     {% endif %}
51
                     {% endif %}
52
                 </td>
52
                 </td>
53
                 {% endfor %}
53
                 {% endfor %}

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

1
 from django import template
1
 from django import template
2
 from django.template import Context
2
 from django.template import Context
3
+from django.template.defaultfilters import floatformat
3
 
4
 
4
 
5
 
5
 register = template.Library()
6
 register = template.Library()
27
     Context({
28
     Context({
28
         'breadcrumbs': breadcrumbs
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
 from django.urls import path
1
 from django.urls import path
2
 from .views import DataListView, DataDetailView, DataCreateView, DataUpdateView, DataDeleteView,\
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
 app_name = 'legacy'  # Namespace for this app
6
 app_name = 'legacy'  # Namespace for this app
6
 
7
 
11
 mg_crud = MgMasterViewCRUDView()
12
 mg_crud = MgMasterViewCRUDView()
12
 bel_crud = BelMasterViewCRUDView()
13
 bel_crud = BelMasterViewCRUDView()
13
 em_crud = EMasterViewCRUDView()
14
 em_crud = EMasterViewCRUDView()
14
-
15
+datarl_crud = DataRLCRUDView()
16
+datawb_crud = DataWbCRUDView()
17
+lsrl_crud = LotSummaryRlCRUDView()
18
+lswb_crud = LotSummaryWbCRUDView()
15
 
19
 
16
 urlpatterns = [
20
 urlpatterns = [
17
     path('data/', DataListView.as_view(), name='data-list'),            # data/
21
     path('data/', DataListView.as_view(), name='data-list'),            # data/
24
     path('datams/create/', datams_crud.get_create_view().as_view(), name='datams-create'),
28
     path('datams/create/', datams_crud.get_create_view().as_view(), name='datams-create'),
25
     path('datams/<int:pk>/update/', datams_crud.get_update_view().as_view(), name='datams-update'),
29
     path('datams/<int:pk>/update/', datams_crud.get_update_view().as_view(), name='datams-update'),
26
     path('datams/<int:pk>/delete/', datams_crud.get_delete_view().as_view(), name='datams-delete'),
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
     path('fg/', fg_crud.get_list_view().as_view(), name='fg-list'),
42
     path('fg/', fg_crud.get_list_view().as_view(), name='fg-list'),
29
     path('fg/create/', fg_crud.get_create_view().as_view(), name='fg-create'),
43
     path('fg/create/', fg_crud.get_create_view().as_view(), name='fg-create'),
34
     path('ls/create/', ls_crud.get_create_view().as_view(), name='ls-create'),
48
     path('ls/create/', ls_crud.get_create_view().as_view(), name='ls-create'),
35
     path('ls/<int:pk>/update/', ls_crud.get_update_view().as_view(), name='ls-update'),
49
     path('ls/<int:pk>/update/', ls_crud.get_update_view().as_view(), name='ls-update'),
36
     path('ls/<int:pk>/delete/', ls_crud.get_delete_view().as_view(), name='ls-delete'),
50
     path('ls/<int:pk>/delete/', ls_crud.get_delete_view().as_view(), name='ls-delete'),
51
+
37
     path('vm/', vm_crud.get_list_view().as_view(), name='vm-list'),
52
     path('vm/', vm_crud.get_list_view().as_view(), name='vm-list'),
38
     path('vm/create/', vm_crud.get_create_view().as_view(), name='vm-create'),
53
     path('vm/create/', vm_crud.get_create_view().as_view(), name='vm-create'),
39
     path('vm/<str:pk>/update/', vm_crud.get_update_view().as_view(), name='vm-update'),
54
     path('vm/<str:pk>/update/', vm_crud.get_update_view().as_view(), name='vm-update'),
55
     path('em/<str:pk>/delete/', em_crud.get_delete_view().as_view(), name='em-delete'),
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
     DeleteView,
11
     DeleteView,
12
 )
12
 )
13
 from django.core.paginator import Paginator
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
 from django.urls import reverse
17
 from django.urls import reverse
17
 from django.contrib import messages
18
 from django.contrib import messages
18
 from pprint import pprint
19
 from pprint import pprint
158
     detail_template_name = 'legacy/datacrud_detail.html'
159
     detail_template_name = 'legacy/datacrud_detail.html'
159
     form_template_name = 'legacy/datacrud_form.html'
160
     form_template_name = 'legacy/datacrud_form.html'
160
     confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
161
     confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
161
-    filterset_class = DataMsFilter
162
+    filterset_class = LotSummaryFilter
162
 
163
 
163
     page_title = "Lot Summary"
164
     page_title = "Lot Summary"
164
 
165
 
301
     # Default ordering
302
     # Default ordering
302
     # ordering = ["-id", "PRO2"]
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
 {
1
 {
2
   "dependencies": {
2
   "dependencies": {
3
     "alpinejs": "^3.14.7",
3
     "alpinejs": "^3.14.7",
4
+    "axios": "^1.7.9",
4
     "font-awesome": "^4.7.0",
5
     "font-awesome": "^4.7.0",
5
     "heroicons": "^2.2.0",
6
     "heroicons": "^2.2.0",
6
     "tailwindcss": "^3.4.17",
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

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
 {% block title %}Report Dashboard{% endblock %}
3
 {% block title %}Report Dashboard{% endblock %}
4
 
4
 
5
 {% block content %}
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
   <h1 class="text-2xl font-bold text-gray-800">Export Center</h1>
8
   <h1 class="text-2xl font-bold text-gray-800">Export Center</h1>
9
   <form method='post'>
9
   <form method='post'>
10
     {% csrf_token %}
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
     <div class="flex items-center gap-2 mb-4">
11
     <div class="flex items-center gap-2 mb-4">
18
       <label for="lot-number" class="text-gray-700 font-medium">Lot No. :</label>
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
       <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type="submit" name="search_lot">
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
       </button>
16
       </button>
23
     </div>
17
     </div>
24
-    <div class="flex items-center gap-4 mb-4">
25
       <button class="bg-blue-100 text-blue-700 px-4 py-2 rounded hover:bg-blue-200">
18
       <button class="bg-blue-100 text-blue-700 px-4 py-2 rounded hover:bg-blue-200">
26
         Option for Export :
19
         Option for Export :
27
       </button>
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
         <label class="flex items-center space-x-1">
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
         </label>
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
   </form>
63
   </form>
67
   {{ result }}
64
   {{ result }}
68
   
65
   
130
 <div class="border-t border-gray-300 my-4"></div>
127
 <div class="border-t border-gray-300 my-4"></div>
131
 {% include "report/_cen.html" %}
128
 {% include "report/_cen.html" %}
132
 {% endblock %}
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
     path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
14
     path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
15
     path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
15
     path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
16
     path('coi/', views.coi_view, name='coi-view'),
16
     path('coi/', views.coi_view, name='coi-view'),
17
+    path('report/generate/', views.gen_report_view, name='gen_report'),
18
+
17
     # path('create/', views.create_report, name='create'),  # Create a new report
19
     # path('create/', views.create_report, name='create'),  # Create a new report
18
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
20
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
19
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report
21
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 223 - 3
app/report/views.py

8
 from .forms import ExportOptionsForm
8
 from .forms import ExportOptionsForm
9
 from pprint import pprint
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
 def index(request):
22
 def index(request):
13
     reports = Report.objects.all()
23
     reports = Report.objects.all()
58
     # config_edit_fields = ["lot_no", "code"]
68
     # config_edit_fields = ["lot_no", "code"]
59
     ordering = ["-created_at", "-id",]
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
 def coi_view(request):
202
 def coi_view(request):
62
     pprint(f"xxxx method = xxx {request.method}")
203
     pprint(f"xxxx method = xxx {request.method}")
204
+    users = User.objects.all()
205
+
63
     if request.method == "POST":
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
         if 'search_lot' in request.POST:
248
         if 'search_lot' in request.POST:
65
             lot_no = request.POST.get('lot_no', None)
249
             lot_no = request.POST.get('lot_no', None)
66
             lot_no = lot_no.strip()
250
             lot_no = lot_no.strip()
78
                                                            'size_str': size_str,
262
                                                            'size_str': size_str,
79
                                                            'spec': spec})
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
         messages.success(request, "Request Sent")
265
         messages.success(request, "Request Sent")
84
         return redirect(request.path_info)
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
 class SysadminConfig(AppConfig):
4
 class SysadminConfig(AppConfig):
5
     default_auto_field = 'django.db.models.BigAutoField'
5
     default_auto_field = 'django.db.models.BigAutoField'
6
     name = 'sysadmin'
6
     name = 'sysadmin'
7
+
8
+    def ready(self):
9
+        import sysadmin.signals

+ 12 - 0
app/sysadmin/filters.py

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
 from django import forms
2
 from django import forms
3
 from django.contrib.auth.forms import AuthenticationForm
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
 class CustomLoginForm(AuthenticationForm):
10
 class CustomLoginForm(AuthenticationForm):
6
     username = forms.CharField(widget=forms.TextInput(attrs={
11
     username = forms.CharField(widget=forms.TextInput(attrs={
7
         'placeholder': 'Username'
12
         'placeholder': 'Username'
11
     }))
16
     }))
12
 
17
 
13
 # forms.py
18
 # forms.py
14
-from django.contrib.auth.forms import UserCreationForm
15
-from django.contrib.auth.models import User
16
 
19
 
17
 class CustomUserCreationForm(UserCreationForm):
20
 class CustomUserCreationForm(UserCreationForm):
18
     class Meta:
21
     class Meta:
33
         self.fields['password2'].widget.attrs.update({
36
         self.fields['password2'].widget.attrs.update({
34
             'placeholder': 'Confirm Password'
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

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
 from django.db import models
1
 from django.db import models
2
 
2
 
3
 # Create your models here.
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

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

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

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
 
3
 
4
 app_name = "sysadmin"  # Use this namespace for reverse URL lookups
4
 app_name = "sysadmin"  # Use this namespace for reverse URL lookups
5
 
5
 
6
+users_crud = views.UserCRUDView()
7
+
6
 urlpatterns = [
8
 urlpatterns = [
7
     path('login/', views.login_view, name='login'),
9
     path('login/', views.login_view, name='login'),
8
     path('register/', views.register_view, name='register'),
10
     path('register/', views.register_view, name='register'),
9
     path('logout/', views.logout_view, name='logout'),
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
 # views.py
1
 # views.py
2
 from django.contrib.auth import authenticate, login, logout
2
 from django.contrib.auth import authenticate, login, logout
3
 from django.shortcuts import render, redirect
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
 def login_view(request):
12
 def login_view(request):
7
     if request.method == "POST":
13
     if request.method == "POST":
30
 def logout_view(request):
36
 def logout_view(request):
31
     logout(request)  # Logs out the user
37
     logout(request)  # Logs out the user
32
     return redirect('sysadmin:login')  # Redirect to the login page after logout
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
 <head>
7
 <head>
8
     <meta charset="UTF-8">
8
     <meta charset="UTF-8">
9
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
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
     <!-- TailwindCSS -->
12
     <!-- TailwindCSS -->
13
     <!--
13
     <!--
16
     <link href="{% static "font-awesome/css/font-awesome.css" %}" rel="stylesheet" />
16
     <link href="{% static "font-awesome/css/font-awesome.css" %}" rel="stylesheet" />
17
     <script type="text/javascript" defer src="{% static "alpinejs/dist/cdn.min.js" %}"></script>
17
     <script type="text/javascript" defer src="{% static "alpinejs/dist/cdn.min.js" %}"></script>
18
     <link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
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
     <script type="text/javascript"  src="{% static "js/main.js" %}"></script>
20
     <script type="text/javascript"  src="{% static "js/main.js" %}"></script>
20
 </head>
21
 </head>
21
 <body class="bg-gray-100 text-gray-800">
22
 <body class="bg-gray-100 text-gray-800">
39
 
40
 
40
                 {% if user.is_authenticated %}
41
                 {% if user.is_authenticated %}
41
                     <!-- Logged-in User -->
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
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44
                 {% else %}
45
                 {% else %}
45
                     <!-- Guest User -->
46
                     <!-- Guest User -->
81
                             <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
                             <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
                           </li>
83
                           </li>
83
                           <li>
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
                             <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>
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
                           </li>
95
                           </li>
86
                           <li>
96
                           <li>
90
                             <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>
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
                           </li>
101
                           </li>
92
                           <li>
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
                             <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>
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
                           </li>
110
                           </li>
95
                           <li>
111
                           <li>
101
                           <li>
117
                           <li>
102
                             <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>
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
                           </li>
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
                     </ul>
120
                     </ul>
108
                  </li>
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
             </ul>
140
             </ul>
111
         </div>
141
         </div>
112
     </aside>
142
     </aside>
144
 
174
 
145
     {% django_browser_reload_script %}
175
     {% django_browser_reload_script %}
146
     <script>
176
     <script>
147
-    // Auto-hide the message after 5 seconds
148
-    setTimeout(() => {
177
+      // Auto-hide the message after 5 seconds
178
+      setTimeout(() => {
149
         const alert = document.getElementById('message-alert');
179
         const alert = document.getElementById('message-alert');
150
         if (alert) {
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
 </body>
188
 </body>
157
 </html>
189
 </html>

BIN
hardness.xlsx


+ 3 - 1
test_xlsx2.py

139
         "customer": "Tum Coder",
139
         "customer": "Tum Coder",
140
         "inspect_date": "2025-01-15",
140
         "inspect_date": "2025-01-15",
141
         "lot_no": "12345",
141
         "lot_no": "12345",
142
+        "staff_name":  "Tum 8888",
143
+        "man_name":  "Tum 999",
142
         "size": "Large",
144
         "size": "Large",
143
         "pcs": "10 pcs",
145
         "pcs": "10 pcs",
144
         "spec": "Spec-A",
146
         "spec": "Spec-A",
149
         "dimension_app.d1_act": "33",
151
         "dimension_app.d1_act": "33",
150
         "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
152
         "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
151
         "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
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
 

tum/coi - Gogs: Simplico Git Service

Ei kuvausta

tum 15f67bc418 dimens 1 vuosi sitten
..
coi f526bca111 coi template 1 vuosi sitten
core 0205d27f6c models 1 vuosi sitten
dashboard e726df91d1 dashboard and restore data 1 vuosi sitten
legacy 0205d27f6c models 1 vuosi sitten
report 15f67bc418 dimens 1 vuosi sitten
static 2399a43570 templates 1 vuosi sitten
sysadmin cae275e4f5 manualsize 1 vuosi sitten
templates 203194ffe1 all product process 1 vuosi sitten
theme 2399a43570 templates 1 vuosi sitten
Mockup_Data_for_MgMasterView.csv 704eaac0a9 coi 1 vuosi sitten
manage.py db1ef2c3ec first commit 1 vuosi sitten
package.json f526bca111 coi template 1 vuosi sitten