143 Commits

Author SHA1 Message Date
komoliddin
fb275a091a Add view for crud user 2026-04-24 11:21:01 +05:00
github-actions[bot]
190480b6f7 🔄 Update image to 114 [CI SKIP] 2026-04-24 05:16:20 +00:00
1a985ffa4b Merge pull request 'Refactor URL patterns for evaluation archiving and remove unused file_url field from BaseCertificateSerializer' (#94) from certificate into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #94
2026-04-24 05:14:45 +00:00
komoliddin
3b62c5a7bf Refactor URL patterns for evaluation archiving and remove unused file_url field from BaseCertificateSerializer 2026-04-24 10:14:01 +05:00
github-actions[bot]
07f8d55966 🔄 Update image to 113 [CI SKIP] 2026-04-23 14:02:58 +00:00
b0b4ccfeee Merge pull request 'makemigrations' (#93) from certificate into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 3m52s
Reviewed-on: #93
2026-04-23 14:01:14 +00:00
komoliddin
ccefe9c119 Merge branch 'main' of https://gitea.felixits.uz/sifatbaho/backend-v1 into certificate 2026-04-23 19:00:42 +05:00
komoliddin
6456283f72 makemigrations 2026-04-23 18:59:06 +05:00
github-actions[bot]
6eed2d998e 🔄 Update image to 112 [CI SKIP] 2026-04-23 13:07:08 +00:00
2c82691166 Merge pull request 'Enhance Certificate model and serializer to support file uploads and URL generation' (#92) from certificate into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 4m27s
Reviewed-on: #92
2026-04-23 13:05:27 +00:00
komoliddin
7a88e39b96 Enhance Certificate model and serializer to support file uploads and URL generation 2026-04-23 18:04:09 +05:00
github-actions[bot]
dc622ce305 🔄 Update image to 111 [CI SKIP] 2026-04-23 11:24:21 +00:00
komoliddin
6e0718c5db merge migrations
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
2026-04-23 16:22:42 +05:00
komoliddin
32d3bea234 Merge branch 'certificate'
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 47s
2026-04-23 16:18:43 +05:00
komoliddin
129827b495 Merge branch 'main' of https://gitea.felixits.uz/sifatbaho/backend-v1
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 49s
2026-04-23 16:13:34 +05:00
komoliddin
50cc555783 Merge branch 'request-archive' 2026-04-23 16:10:00 +05:00
komoliddin
76563b3ef0 Add Certificate model and write crud for it 2026-04-23 16:07:37 +05:00
github-actions[bot]
207363dc6a 🔄 Update image to 108 [CI SKIP] 2026-04-23 10:50:59 +00:00
2a08ad9662 Merge pull request 'Add is_archive field to Quickevaluation model. Write apis for update is_archive and list archived quick evaluations' (#89) from tb-archive into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 3m58s
Reviewed-on: #89
2026-04-23 10:49:11 +00:00
komoliddin
5cf4b950fb Make migrations 2026-04-23 12:25:18 +05:00
komoliddin
3a08c81ff3 Add is_archive field to EvaluationRequest model. Write apis for update is_archive and list archived requests 2026-04-23 12:23:55 +05:00
komoliddin
4fee037467 Add is_archive field to Quickevaluation model. Write apis for update is_archive and list archived quick evaluations 2026-04-23 11:49:55 +05:00
github-actions[bot]
320f490d23 🔄 Update image to 107 [CI SKIP] 2026-04-22 10:03:44 +00:00
d4e6d80c86 Merge pull request 'deploy.yaml fixess' (#88) from fix/cicd-env into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #88
2026-04-22 10:02:12 +00:00
1f0e942be8 deploy.yaml fixess 2026-04-22 15:01:52 +05:00
github-actions[bot]
2af67333e2 🔄 Update image to 106 [CI SKIP] 2026-04-22 09:21:04 +00:00
81c689e7e9 Merge pull request 'feat: add 404 response for missing company or person in Didox API' (#86) from didox-api into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #86
2026-04-22 09:19:30 +00:00
github-actions[bot]
09d2e0954c 🔄 Update image to 105 [CI SKIP] 2026-04-22 09:17:53 +00:00
84a5afb2ee Merge pull request 'add request status change api' (#87) from shaxob into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #87
Reviewed-by: xoliqberdiyev <behruz@felixits.uz>
2026-04-22 09:16:19 +00:00
github-actions[bot]
cc8fc345d9 🔄 Update image to 104 [CI SKIP] 2026-04-22 09:11:31 +00:00
03e4387e4d Merge pull request 'fix: handle response errors and improve data return in TechPassportService' (#85) from tech-passport into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m16s
Reviewed-on: #85
Reviewed-by: xoliqberdiyev <behruz@felixits.uz>
2026-04-22 09:09:38 +00:00
Shaxobff
64993805af add request status change api 2026-04-22 13:52:39 +05:00
komoliddin
01711b5927 feat: add 404 response for missing company or person in Didox API 2026-04-22 11:28:35 +05:00
komoliddin
33aa06f80b fix: handle response errors and improve data return in TechPassportService 2026-04-22 11:17:09 +05:00
github-actions[bot]
cb2d83b00d 🔄 Update image to 103 [CI SKIP] 2026-04-21 12:36:45 +00:00
46c9621457 Merge pull request 'sort yangilandi' (#84) from fix/evaluation-request-api into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #84
2026-04-21 12:35:14 +00:00
github-actions[bot]
8d73fa09a7 🔄 Update image to 102 [CI SKIP] 2026-04-21 11:43:22 +00:00
3367063707 Merge pull request 'write swagger docs' (#82) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #82
2026-04-21 11:41:48 +00:00
xoliqberdiyev
ae926914a5 write swagger docs 2026-04-21 16:41:37 +05:00
github-actions[bot]
229676be5c 🔄 Update image to 101 [CI SKIP] 2026-04-21 11:27:41 +00:00
d2f517baf4 Merge pull request 'feat: uncomment significant code for ws' (#81) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #81
2026-04-21 11:26:08 +00:00
xoliqberdiyev
6df20f35c2 feat: uncomment significant code for ws 2026-04-21 16:25:51 +05:00
github-actions[bot]
fa676bfa96 🔄 Update image to 100 [CI SKIP] 2026-04-21 11:13:53 +00:00
ca9f12ae32 Merge pull request 'fix' (#80) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #80
2026-04-21 11:12:18 +00:00
xoliqberdiyev
56f693abfd fix 2026-04-21 16:12:06 +05:00
github-actions[bot]
5049919865 🔄 Update image to 99 [CI SKIP] 2026-04-21 10:29:59 +00:00
ca3c9bfe4a Merge pull request 'feat: removing location fields from auto-evalutaion model, adding new tex_passport_file field for auto-evalutaion model' (#79) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m0s
Reviewed-on: #79
2026-04-21 10:28:22 +00:00
xoliqberdiyev
8d8744731a feat: removing location fields from auto-evalutaion model, adding new tex_passport_file field for auto-evalutaion model 2026-04-21 15:28:03 +05:00
github-actions[bot]
7ab1e0b1e0 🔄 Update image to 98 [CI SKIP] 2026-04-21 10:23:04 +00:00
f66a35ec80 Merge pull request 'remote: tex_passport_file field removed from quick evaluation serializers' (#78) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #78
2026-04-21 10:21:29 +00:00
xoliqberdiyev
f71dfa50c1 remote: tex_passport_file field removed from quick evaluation serializers 2026-04-21 15:21:14 +05:00
github-actions[bot]
f68f34178e 🔄 Update image to 97 [CI SKIP] 2026-04-21 10:20:39 +00:00
ab68a6a463 Merge pull request 'feat: add Tech Passport API integration for vehicle information retrieval' (#76) from tech-passport into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m0s
Reviewed-on: #76
Reviewed-by: xoliqberdiyev <behruz@felixits.uz>
2026-04-21 10:19:01 +00:00
github-actions[bot]
d246406384 🔄 Update image to 96 [CI SKIP] 2026-04-21 10:15:49 +00:00
0a74f71efa Merge pull request 'fix: change the evaluation-request when creating auto evaluation to this evaluation-request' (#77) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m2s
Reviewed-on: #77
2026-04-21 10:14:13 +00:00
xoliqberdiyev
575364e0ef fix: change the evaluation-request when creating auto evaluation to this evaluation-request 2026-04-21 15:13:41 +05:00
komoliddin
406477fc33 feat: add Tech Passport API integration for vehicle information retrieval 2026-04-21 15:11:46 +05:00
github-actions[bot]
2aa73e15a0 🔄 Update image to 95 [CI SKIP] 2026-04-21 09:52:02 +00:00
372a289447 Merge pull request 'change region models name field from unique to typical' (#75) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m58s
Reviewed-on: #75
2026-04-21 09:50:28 +00:00
xoliqberdiyev
7343f6191d change region models name field from unique to typical 2026-04-21 14:48:41 +05:00
github-actions[bot]
5c4d5fbb71 🔄 Update image to 94 [CI SKIP] 2026-04-21 09:47:17 +00:00
20043db5b3 Merge pull request 'change ws response' (#74) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m1s
Reviewed-on: #74
2026-04-21 09:45:41 +00:00
xoliqberdiyev
7d6618155f change ws response 2026-04-21 14:45:01 +05:00
github-actions[bot]
11605e6e6c 🔄 Update image to 93 [CI SKIP] 2026-04-21 08:07:30 +00:00
komoliddin
8a1a66a05d feat: add Didox service integration for company info retrieval
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m9s
2026-04-21 13:05:45 +05:00
github-actions[bot]
70555fa93a 🔄 Update image to 92 [CI SKIP] 2026-04-20 13:12:55 +00:00
674eafe65b Merge pull request 'fix' (#73) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m59s
Reviewed-on: #73
2026-04-20 13:11:20 +00:00
xoliqberdiyev
47833176e2 fix 2026-04-20 18:10:51 +05:00
b03d2e1c5e Merge pull request 'add new fields' (#72) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 31s
Reviewed-on: #72
2026-04-20 13:06:21 +00:00
xoliqberdiyev
7be8999a39 add new fields 2026-04-20 18:06:06 +05:00
github-actions[bot]
c5624e361d 🔄 Update image to 90 [CI SKIP] 2026-04-20 13:02:55 +00:00
c44b08bb28 Merge pull request 'fix' (#71) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m59s
Reviewed-on: #71
2026-04-20 13:01:19 +00:00
xoliqberdiyev
d448324f89 fix 2026-04-20 18:01:00 +05:00
github-actions[bot]
0508a49c1d 🔄 Update image to 89 [CI SKIP] 2026-04-20 10:38:57 +00:00
8b97ca53bb Merge pull request 'fix' (#70) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #70
2026-04-20 10:37:24 +00:00
xoliqberdiyev
2d64777041 fix 2026-04-20 15:37:06 +05:00
github-actions[bot]
9f7b29fa13 🔄 Update image to 88 [CI SKIP] 2026-04-20 10:35:28 +00:00
b4a3243e9c Merge pull request 'gi' (#69) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #69
2026-04-20 10:33:56 +00:00
xoliqberdiyev
63359e69f7 gi 2026-04-20 15:33:04 +05:00
github-actions[bot]
3c9438aff6 🔄 Update image to 87 [CI SKIP] 2026-04-20 10:31:42 +00:00
a3c67c5bfb Merge pull request 'remove name_ru and name_en fields' (#68) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #68
2026-04-20 10:30:11 +00:00
xoliqberdiyev
e768c73d54 remove name_ru and name_en fields 2026-04-20 15:29:59 +05:00
github-actions[bot]
a2684ff749 🔄 Update image to 86 [CI SKIP] 2026-04-20 10:27:16 +00:00
91bbb6b6e5 Merge pull request 'change file name' (#67) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #67
2026-04-20 10:25:44 +00:00
xoliqberdiyev
01a70debaf change file name 2026-04-20 15:25:28 +05:00
github-actions[bot]
ae9f10aa4d 🔄 Update image to 85 [CI SKIP] 2026-04-20 10:16:13 +00:00
f2dc2e1d52 Merge pull request 'add migrations files in shared app' (#66) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #66
2026-04-20 10:14:39 +00:00
xoliqberdiyev
7081a03a98 add migrations files in shared app 2026-04-20 15:14:26 +05:00
github-actions[bot]
10646e2ebf 🔄 Update image to 84 [CI SKIP] 2026-04-20 10:09:49 +00:00
93f6e79f58 Merge pull request 'fix' (#65) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #65
2026-04-20 10:08:16 +00:00
xoliqberdiyev
3b95734ea4 fix 2026-04-20 15:08:04 +05:00
d1aad04ab8 Merge pull request 'registring region,district and village models' (#64) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 51s
Reviewed-on: #64
2026-04-20 10:06:37 +00:00
xoliqberdiyev
adf3c9fff9 registring region,district and village models 2026-04-20 15:06:06 +05:00
github-actions[bot]
7e4a90fe2a 🔄 Update image to 82 [CI SKIP] 2026-04-20 05:46:19 +00:00
503c16bdb8 Merge pull request 'swagger to'g'irlandi' (#63) from fix/swagger into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #63
2026-04-20 05:44:45 +00:00
701a375e3c swagger to'g'irlandi 2026-04-20 10:44:23 +05:00
github-actions[bot]
e64d7d037f 🔄 Update image to 81 [CI SKIP] 2026-04-18 11:56:52 +00:00
b7ecf0e47c Merge pull request 'fix' (#62) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m54s
Reviewed-on: #62
2026-04-18 11:55:21 +00:00
xoliqberdiyev
c7c76e9c79 fix 2026-04-18 16:55:02 +05:00
github-actions[bot]
afdc6aa0ac 🔄 Update image to 80 [CI SKIP] 2026-04-18 11:52:43 +00:00
f991e3e4ec Merge pull request 'behruz' (#61) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m57s
Reviewed-on: #61
2026-04-18 11:51:08 +00:00
xoliqberdiyev
0f142be77b fix 2026-04-18 16:50:48 +05:00
xoliqberdiyev
182b2483bf fix 2026-04-18 16:48:32 +05:00
469659baa8 Merge pull request 'fix' (#60) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 1m17s
Reviewed-on: #60
2026-04-18 11:32:05 +00:00
xoliqberdiyev
965328edd4 fix 2026-04-18 16:31:53 +05:00
github-actions[bot]
11aa88a2b0 🔄 Update image to 78 [CI SKIP] 2026-04-18 11:25:47 +00:00
225565bc41 Merge pull request 'add extra user fields for chat' (#59) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #59
2026-04-18 11:24:13 +00:00
xoliqberdiyev
75365f8e7e add extra user fields for chat 2026-04-18 16:24:02 +05:00
github-actions[bot]
3e9675a4be 🔄 Update image to 77 [CI SKIP] 2026-04-18 11:22:09 +00:00
c3cf7ac9d5 Merge pull request 'add all fields to ordering_fields filter' (#58) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m55s
Reviewed-on: #58
2026-04-18 11:20:36 +00:00
xoliqberdiyev
5dcafb9cc1 add all fields to ordering_fields filter 2026-04-18 16:20:19 +05:00
github-actions[bot]
62bc6c17b8 🔄 Update image to 76 [CI SKIP] 2026-04-17 13:46:12 +00:00
22284f228f Merge pull request 'fi' (#57) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m56s
Reviewed-on: #57
2026-04-17 13:44:40 +00:00
xoliqberdiyev
aa487c1557 fi 2026-04-17 18:44:18 +05:00
github-actions[bot]
73d5222a5b 🔄 Update image to 75 [CI SKIP] 2026-04-17 13:35:44 +00:00
707ea1e1fd Merge pull request 'add celery to docker-compose file' (#56) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m52s
Reviewed-on: #56
2026-04-17 13:34:12 +00:00
xoliqberdiyev
bfbc14db82 add celery to docker-compose file 2026-04-17 18:33:58 +05:00
github-actions[bot]
4c1f1d7104 🔄 Update image to 74 [CI SKIP] 2026-04-17 13:26:45 +00:00
38e7aac505 Merge pull request 'fix' (#55) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m50s
Reviewed-on: #55
2026-04-17 13:25:14 +00:00
xoliqberdiyev
a973da91f5 fix 2026-04-17 18:24:59 +05:00
github-actions[bot]
ec94e4cdd0 🔄 Update image to 73 [CI SKIP] 2026-04-17 13:21:05 +00:00
87701c31f1 Merge pull request 'fix' (#54) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m51s
Reviewed-on: #54
2026-04-17 13:19:34 +00:00
xoliqberdiyev
35739ddc27 fix 2026-04-17 18:19:12 +05:00
e8feafca0b Merge pull request 'add new task for sending new messages to the chat' (#53) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 48s
Reviewed-on: #53
2026-04-17 13:15:06 +00:00
xoliqberdiyev
505c33a554 add new task for sending new messages to the chat 2026-04-17 18:14:24 +05:00
github-actions[bot]
f24afaf55c 🔄 Update image to 71 [CI SKIP] 2026-04-17 10:15:24 +00:00
665db58bbc Merge pull request 'fix' (#52) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m50s
Reviewed-on: #52
2026-04-17 10:13:52 +00:00
xoliqberdiyev
57e22e04db fix 2026-04-17 15:13:36 +05:00
github-actions[bot]
528dc57ea2 🔄 Update image to 70 [CI SKIP] 2026-04-17 10:13:21 +00:00
774be87f45 Merge pull request 'feat: add new extra fields for auto-evalution list serializer' (#51) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m50s
Reviewed-on: #51
2026-04-17 10:11:48 +00:00
xoliqberdiyev
3d0141197f feat: add new extra fields for auto-evalution list serializer 2026-04-17 15:11:26 +05:00
9dc093e244 Merge pull request 'feat: auto-evaluation update api serializer class changed' (#50) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 1m19s
Reviewed-on: #50
2026-04-17 10:01:57 +00:00
xoliqberdiyev
aa1e4ca6fc feat: auto-evaluation update api serializer class changed 2026-04-17 15:01:25 +05:00
github-actions[bot]
04ef567d5c 🔄 Update image to 68 [CI SKIP] 2026-04-16 10:33:44 +00:00
b604fff1c2 Merge pull request 'fi' (#49) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m51s
Reviewed-on: #49
2026-04-16 10:32:13 +00:00
xoliqberdiyev
277bc2874e fi 2026-04-16 15:31:49 +05:00
github-actions[bot]
37459e8485 🔄 Update image to 67 [CI SKIP] 2026-04-16 10:28:48 +00:00
f231d889a5 Merge pull request 'add base url' (#48) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m49s
Reviewed-on: #48
2026-04-16 10:27:19 +00:00
xoliqberdiyev
3fa70d9436 add base url 2026-04-16 15:26:47 +05:00
github-actions[bot]
6b1a9439ce 🔄 Update image to 66 [CI SKIP] 2026-04-16 10:15:07 +00:00
d26e2b0147 Merge pull request 'fix' (#47) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m49s
Reviewed-on: #47
2026-04-16 10:13:37 +00:00
xoliqberdiyev
da71ce6d28 fix 2026-04-16 15:13:19 +05:00
Husanjonazamov
8207b750b8 sort yangilandi 2026-03-12 17:29:10 +05:00
64 changed files with 24532 additions and 184 deletions

View File

@@ -73,6 +73,9 @@ STORAGE_BUCKET_STATIC=name
STORAGE_PATH=127.0.0.1:8081/bucket/
STORAGE_PROTOCOL=http:
# Didox configs
DIDOX_PARTNER_TOKEN=...
# Celery configs

View File

@@ -153,7 +153,7 @@ jobs:
update_env \
"DB_HOST=postgres" \
"DB_NAME=sifatbahodb" \
"DB_PORT=5432"
"DB_PORT=5432" \
"DIDOX_TOKEN=${{ secrets.DIDOX_TOKEN }}"
export PORT=8085
docker stack deploy -c stack.yaml ${{ env.PROJECT_NAME }} --with-registry-auth

View File

@@ -165,5 +165,26 @@ PAGES = [
"link": reverse_lazy("admin:evaluation_documentcategorymodel_changelist"),
}
]
},
{
"title": _("Joylashuvlar"),
"separator": True,
"items": [
{
"title": _("Shaharlar"),
"icon": "attach_file",
"link": reverse_lazy("admin:shared_regionmodel_changelist"),
},
{
"title": _("Tumanlar"),
"icon": "attach_file",
"link": reverse_lazy("admin:shared_districtmodel_changelist"),
},
{
"title": _("Mahallalar"),
"icon": "attach_file",
"link": reverse_lazy("admin:shared_villagemodel_changelist"),
},
]
}
]

View File

@@ -9,7 +9,7 @@ import environ
environ.Env.read_env(os.path.join(".env"))
env = environ.Env(
DEBUG=(bool, False),
DEBUG=(bool, True),
CACHE_TIME=(int, 180),
OTP_EXPIRE_TIME=(int, 2),
VITE_LIVE=(bool, False),
@@ -26,4 +26,5 @@ env = environ.Env(
OTP_SERVICE="EskizService",
PROJECT_ENV=(str, "prod"),
SILK_ENABLED=(bool, False),
DIDOX_PARTNER_TOKEN=(str),
)

View File

@@ -49,6 +49,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.postgres",
] + APPS
MODULES = [app for app in MODULES if isinstance(app, str)]
@@ -165,6 +166,8 @@ SITE_URL = env.str("SITE_URL", default="http://localhost:8055")
MODELTRANSLATION_LANGUAGES = ("uz", "ru", "en")
MODELTRANSLATION_DEFAULT_LANGUAGE = "uz"
DIDOX_PARTNER_TOKEN = env.str("DIDOX_PARTNER_TOKEN")
JST_LANGUAGES = [

View File

@@ -13,7 +13,7 @@ from config.env import env
def home(request):
return HttpResponse("OK: #9c40908cc7c09be627fdcea67fbb22999133b242")
return HttpResponse("OK: #1a985ffa4b785b63a71b9e0cdd78042c3fcda239")
urlpatterns = [

View File

@@ -3,6 +3,8 @@ from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
avatar = serializers.SerializerMethodField(method_name='get_avatar')
class Meta:
exclude = [
"created_at",
@@ -10,10 +12,15 @@ class UserSerializer(serializers.ModelSerializer):
"password",
"groups",
"user_permissions",
"role"
]
model = get_user_model()
def get_avatar(self, obj):
request = self.context.get('request')
if obj.avatar:
return request.build_absolute_uri(obj.avatar.url)
return None
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
@@ -23,3 +30,16 @@ class UserUpdateSerializer(serializers.ModelSerializer):
"last_name",
"avatar"
]
class AdminUserSerializer(serializers.ModelSerializer):
avatar = serializers.SerializerMethodField(method_name='get_avatar')
class Meta:
model = get_user_model()
fields = "__all__"
def get_avatar(self, obj):
request = self.context.get('request')
if obj.avatar:
return request.build_absolute_uri(obj.avatar.url)
return None

View File

@@ -4,7 +4,7 @@ Accounts app urls
from django.urls import path, include
from rest_framework_simplejwt import views as jwt_views
from .views import RegisterView, ResetPasswordView, MeView, ChangePasswordView, UserListApiView, AdminUserListApiView
from .views import RegisterView, ResetPasswordView, MeView, ChangePasswordView, UserListApiView, AdminUserListApiView,AdminUserView
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
@@ -12,6 +12,7 @@ router.register("auth", RegisterView, basename="auth")
router.register("auth", ResetPasswordView, basename="reset-password")
router.register("auth", MeView, basename="me")
router.register("auth", ChangePasswordView, basename="change-password")
router.register("user", AdminUserView, basename="user-crud")
urlpatterns = [

View File

@@ -177,11 +177,11 @@ class MeView(BaseViewSetMixin, GenericViewSet, UserService):
@action(methods=["GET", "OPTIONS"], detail=False, url_path="me")
def me(self, request):
return Response(self.get_serializer(request.user).data)
return Response(UserSerializer(request.user, context={"request": request}).data)
@action(methods=["PATCH", "PUT"], detail=False, url_path="user-update")
def user_update(self, request):
ser = self.get_serializer(instance=request.user, data=request.data, partial=True)
ser = self.get_serializer(instance=request.user, data=request.data, partial=True, context={"request": request})
ser.is_valid(raise_exception=True)
data = ser.save()
return Response(UserSerializer(data).data)

View File

@@ -5,8 +5,11 @@ from rest_framework.permissions import IsAuthenticated
from drf_spectacular.utils import extend_schema
from core.apps.accounts.serializers.user import UserSerializer
from core.apps.accounts.serializers.user import UserSerializer, AdminUserSerializer
from core.apps.accounts.choices.user import RoleChoice
from django_core.mixins import BaseViewSetMixin
from rest_framework.viewsets import ModelViewSet
User = get_user_model()
@@ -18,6 +21,9 @@ class UserListApiView(generics.ListAPIView):
filter_backends = [filters.SearchFilter]
search_fields = ['phone', 'first_name', 'last_name']
def serializer_context(self):
return self.serializer_class(context={"request": self.request})
@extend_schema(tags=['User'])
class AdminUserListApiView(generics.ListAPIView):
@@ -26,3 +32,16 @@ class AdminUserListApiView(generics.ListAPIView):
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ['phone', 'first_name', 'last_name']
@extend_schema(tags=["User"],request=AdminUserSerializer)
class AdminUserView(BaseViewSetMixin, ModelViewSet):
queryset = User.objects.filter(role=RoleChoice.USER)
serializer_class = AdminUserSerializer
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ['phone', 'first_name', 'last_name']
def serializer_context(self):
return self.serializer_class(context={"request": self.request})

View File

@@ -4,6 +4,11 @@ from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import AnonymousUser
def get_base_url(scope):
headers = dict(scope["headers"])
host = headers.get(b"host", b"").decode()
scheme = "https" if scope.get("scheme") == "https" else "http"
return f"{scheme}://{host}"
class ChatConsumer(AsyncWebsocketConsumer):
"""
@@ -36,7 +41,7 @@ class ChatConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data):
user = self.scope.get("user")
if not user or isinstance(user, AnonymousUser):
await self.close(code=4001)
await self.close(code=401)
return
try:
@@ -48,13 +53,10 @@ class ChatConsumer(AsyncWebsocketConsumer):
message_type = data.get("message_type", "text")
text = (data.get("text") or "").strip()
# Matn xabari uchun text majburiy
if message_type == "text" and not text:
await self.send(text_data=json.dumps({"error": "Matn bo'sh bo'lishi mumkin emas."}))
return
# WS orqali faqat matn + caption saqlanadi.
# Fayl yuklash uchun REST /chat/messages/ POST ishlatiladi.
if message_type != "text":
await self.send(
text_data=json.dumps(
@@ -63,9 +65,9 @@ class ChatConsumer(AsyncWebsocketConsumer):
)
return
# DB ga saqlash — post_save signal WS ga broadcast qiladi
await self._save_message(user, text)
async def chat_message(self, event):
await self.send(
text_data=json.dumps(
@@ -91,6 +93,12 @@ class ChatConsumer(AsyncWebsocketConsumer):
text=text,
)
full_name = user.get_full_name().strip() or str(user.phone)
base_url = get_base_url(self.scope)
avatar_url = (
base_url + user.avatar.url
if user.avatar else None
)
return {
"id": msg.id,
"message_type": msg.message_type,
@@ -100,6 +108,8 @@ class ChatConsumer(AsyncWebsocketConsumer):
"id": user.id,
"full_name": full_name,
"role": user.role,
"phone": user.phone,
"avatar": avatar_url,
},
"created_at": msg.created_at.isoformat(),
}

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from core.apps.chat.models import ChatmessageModel
from core.apps.chat.tasks.message import send_message_to_chat
class BaseChatmessageSerializer(serializers.ModelSerializer):
sender = serializers.SerializerMethodField()
@@ -13,10 +13,13 @@ class BaseChatmessageSerializer(serializers.ModelSerializer):
full_name = obj.sender.get_full_name().strip()
if not full_name:
full_name = str(obj.sender.phone)
request = self.context.get("request")
return {
"id": obj.sender.id,
"full_name": full_name,
"role": obj.sender.role,
"phone": obj.sender.phone,
"avatar": request.build_absolute_uri(obj.sender.avatar.url) if obj.sender.avatar else None,
}
def get_file_url(self, obj):
@@ -72,4 +75,9 @@ class CreateChatmessageSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data["sender"] = self.context["request"].user
return super().create(validated_data)
request = self.context["request"]
message = super().create(validated_data)
file_url = request.build_absolute_uri(message.file.url) if message.file else None
avatar_url = request.build_absolute_uri(self.context['request'].user.avatar.url) if self.context['request'].user.avatar else None
send_message_to_chat.delay(message.id, file_url, avatar_url)
return message

View File

@@ -7,42 +7,42 @@ from django.dispatch import receiver
from core.apps.chat.models import ChatmessageModel, ChatroomModel
@receiver(post_save, sender=ChatmessageModel)
def broadcast_new_message(sender, instance, created, **kwargs):
"""Yangi xabar saqlanganda xonadagi barcha WS ulanishlariga yuboradi."""
if not created:
return
# @receiver(post_save, sender=ChatmessageModel)
# def broadcast_new_message(sender, instance, created, **kwargs):
# """Yangi xabar saqlanganda xonadagi barcha WS ulanishlariga yuboradi."""
# if not created:
# return
channel_layer = get_channel_layer()
if channel_layer is None:
return
# channel_layer = get_channel_layer()
# if channel_layer is None:
# return
sender_obj = instance.sender
if sender_obj:
full_name = sender_obj.get_full_name().strip() or str(sender_obj.phone)
sender_data = {
"id": sender_obj.id,
"full_name": full_name,
"role": sender_obj.role,
}
else:
sender_data = None
# sender_obj = instance.sender
# if sender_obj:
# full_name = sender_obj.get_full_name().strip() or str(sender_obj.phone)
# sender_data = {
# "id": sender_obj.id,
# "full_name": full_name,
# "role": sender_obj.role,
# }
# else:
# sender_data = None
site_url = getattr(settings, "SITE_URL", "").rstrip("/")
file_url = (site_url + instance.file.url) if instance.file else None
# site_url = getattr(settings, "SITE_URL", "").rstrip("/")
# file_url = (site_url + instance.file.url) if instance.file else None
async_to_sync(channel_layer.group_send)(
f"chat_room_{instance.room_id}",
{
"type": "chat_message",
"id": instance.id,
"message_type": instance.message_type,
"text": instance.text,
"file_url": file_url,
"sender": sender_data,
"created_at": instance.created_at.isoformat(),
},
)
# async_to_sync(channel_layer.group_send)(
# f"chat_room_{instance.room_id}",
# {
# "type": "chat_message",
# "id": instance.id,
# "message_type": instance.message_type,
# "text": instance.text,
# "file_url": file_url,
# "sender": sender_data,
# "created_at": instance.created_at.isoformat(),
# },
# )
@receiver(post_save, sender="evaluation.AutoEvaluationModel")

View File

@@ -0,0 +1 @@
from .message import send_message_to_chat

View File

@@ -0,0 +1,44 @@
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from celery import shared_task
from core.apps.chat.models import ChatmessageModel
@shared_task
def send_message_to_chat(message_id, file_url, avatar_url):
try:
message = ChatmessageModel.objects.get(id=message_id)
except ChatmessageModel.DoesNotExist:
return "Not found"
channel_layer = get_channel_layer()
if channel_layer is None:
return
sender_obj = message.sender
if sender_obj:
full_name = sender_obj.get_full_name().strip() or str(sender_obj.phone)
sender_data = {
"id": sender_obj.id,
"full_name": full_name,
"role": sender_obj.role,
"phone": sender_obj.phone,
"avatar": avatar_url,
}
else:
sender_data = None
async_to_sync(channel_layer.group_send)(
f"chat_room_{message.room_id}",
{
"type": "chat_message",
"id": message.id,
"message_type": message.message_type,
"text": message.text,
"file_url": file_url,
"sender": sender_data,
"created_at": message.created_at.isoformat(),
},
)

View File

@@ -50,15 +50,8 @@ class AutoEvaluationAdmin(ModelAdmin):
("value_determined", "rate_type"),
),
}),
("Step 3 — Manzil ma'lumotlari", {
"fields": (
("object_location_province", "object_location_district"),
("object_location_city", "object_location_neighborhood"),
("object_location_street", "object_location_home"),
("object_location_highways", "object_location_covenience"),
),
}),
("Step 4 — Avtomobil ma'lumotlari", {
("Step 3 — Avtomobil ma'lumotlari", {
"fields": (
"tex_passport_serie_num",
("tex_passport_gived_date", "tex_passport_gived_location"),

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.2.7 on 2026-04-18 11:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0028_autoevaluationmodel_appraisers'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='autoevaluationmodel',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='auto_evaluations_user', to=settings.AUTH_USER_MODEL, verbose_name='user'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2026-04-20 13:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0029_autoevaluationmodel_user'),
]
operations = [
migrations.AddField(
model_name='autoevaluationmodel',
name='evaluation_request',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='auto_evaluations_request', to='evaluation.evaluationrequestmodel', verbose_name='evaluation request'),
),
]

View File

@@ -0,0 +1,54 @@
# Generated by Django 5.2.7 on 2026-04-21 10:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0030_autoevaluationmodel_evaluation_request'),
]
operations = [
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_city',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_covenience',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_district',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_highways',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_home',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_neighborhood',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_province',
),
migrations.RemoveField(
model_name='autoevaluationmodel',
name='object_location_street',
),
migrations.RemoveField(
model_name='quickevaluationmodel',
name='tex_passport_file',
),
migrations.AddField(
model_name='autoevaluationmodel',
name='tex_passport_file',
field=models.FileField(blank=True, null=True, upload_to='quick_evaluation/tech_passports/%Y/%m/', verbose_name='tech passport file'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.4 on 2026-04-23 11:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'),
]
operations = [
migrations.CreateModel(
name='CertificateModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=255, verbose_name='title')),
('file_url', models.URLField(max_length=255, verbose_name='file url')),
],
options={
'verbose_name': 'Certificate',
'verbose_name_plural': 'Certificates',
'db_table': 'certificate',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-23 07:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'),
]
operations = [
migrations.AddField(
model_name='evaluationrequestmodel',
name='is_archive',
field=models.BooleanField(default=False, verbose_name='is archive'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-23 07:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'),
]
operations = [
migrations.AddField(
model_name='quickevaluationmodel',
name='is_archive',
field=models.BooleanField(default=False, verbose_name='is archive'),
),
]

View File

@@ -0,0 +1,15 @@
# Generated by Django 6.0.4 on 2026-04-23 11:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0032_certificatemodel'),
('evaluation', '0032_evaluationrequestmodel_is_archive'),
('evaluation', '0032_quickevaluationmodel_is_archive'),
]
operations = [
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 6.0.4 on 2026-04-23 13:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('evaluation', '0033_merge_20260423_1622'),
]
operations = [
migrations.RemoveField(
model_name='certificatemodel',
name='file_url',
),
migrations.AddField(
model_name='certificatemodel',
name='file',
field=models.FileField(blank=True, null=True, upload_to='certificates/', verbose_name='file'),
),
]

View File

@@ -11,3 +11,4 @@ from .report import * # noqa
from .request import * # noqa
from .valuation import * # noqa
from .vehicle import * # noqa
from .certificate import * # noqa

View File

@@ -22,6 +22,22 @@ from .vehicle import VehicleModel
class AutoEvaluationModel(AbstractBaseModel):
user = models.ForeignKey(
"accounts.User",
on_delete=models.SET_NULL,
related_name="auto_evaluations_user",
verbose_name=_("user"),
null=True,
blank=True,
)
evaluation_request = models.ForeignKey(
"evaluation.EvaluationRequestModel",
on_delete=models.SET_NULL,
related_name="auto_evaluations_request",
verbose_name=_("evaluation request"),
null=True,
blank=True,
)
valuation = models.OneToOneField(
ValuationModel,
on_delete=models.CASCADE,
@@ -46,6 +62,13 @@ class AutoEvaluationModel(AbstractBaseModel):
null=True,
)
tex_passport_file = models.FileField(
verbose_name=_("tech passport file"),
upload_to="quick_evaluation/tech_passports/%Y/%m/",
blank=True,
null=True,
)
# ── Step 1 — Umumiy ma'lumotlar ──────────────────────────────────
registration_number = models.CharField(
verbose_name=_("registration number"),
@@ -164,56 +187,6 @@ class AutoEvaluationModel(AbstractBaseModel):
related_name='evaluation_auto_rate_type'
)
# ── Step 3 — Manzil ma'lumotlari ────────────────────────────────
object_location_province = models.CharField(
verbose_name=_("object location province"),
max_length=100,
blank=True,
null=True,
)
object_location_district = models.CharField(
verbose_name=_("object location district"),
max_length=100,
blank=True,
null=True,
)
object_location_city = models.CharField(
verbose_name=_("object location city"),
max_length=100,
blank=True,
null=True,
)
object_location_neighborhood = models.CharField(
verbose_name=_("object location neighborhood"),
max_length=100,
blank=True,
null=True,
)
object_location_street = models.CharField(
verbose_name=_("object location street"),
max_length=100,
blank=True,
null=True,
)
object_location_home = models.CharField(
verbose_name=_("object location home"),
max_length=50,
blank=True,
null=True,
)
object_location_highways = models.IntegerField(
verbose_name=_("location highways"),
choices=LocationHighways.choices,
blank=True,
null=True,
)
object_location_covenience = models.IntegerField(
verbose_name=_("location convenience"),
choices=LocationConvenience.choices,
blank=True,
null=True,
)
# ── Step 4 — Avtomobil ma'lumotlari ─────────────────────────────
tex_passport_serie_num = models.CharField(
verbose_name=_("tech passport series and number"),

View File

@@ -0,0 +1,30 @@
from django.db import models
from django_core.models import AbstractBaseModel
from django.utils.translation import gettext_lazy as _
from model_bakery import baker
class CertificateModel(AbstractBaseModel):
title = models.CharField(
verbose_name=_("title"),
max_length=255
)
file = models.FileField(
verbose_name=_("file"),
upload_to="certificates/",
blank=True,
null=True
)
def __str__(self):
return self.title
@classmethod
def _baker(cls):
return baker.make(cls)
class Meta:
db_table = "certificate"
verbose_name = _("Certificate")
verbose_name_plural = _("Certificates")

View File

@@ -34,12 +34,6 @@ class QuickEvaluationModel(AbstractBaseModel):
blank=True,
null=True,
)
tex_passport_file = models.FileField(
verbose_name=_("tech passport file"),
upload_to="quick_evaluation/tech_passports/%Y/%m/",
blank=True,
null=True,
)
# Car info
car_type = models.CharField(
@@ -138,6 +132,11 @@ class QuickEvaluationModel(AbstractBaseModel):
default=QuickEvaluationStatus.CREATED,
)
is_archive = models.BooleanField(
verbose_name=_("is archive"),
default=False,
)
def __str__(self):
return f"Quick Evaluation {self.pk} by {self.created_by}"

View File

@@ -118,9 +118,12 @@ class EvaluationrequestModel(AbstractBaseModel):
choices=RequestStatus.choices,
default=RequestStatus.PENDING,
)
is_archive = models.BooleanField(
verbose_name=_("is archive"),
default=False,
)
def __str__(self):
return f"Request #{self.pk}{self.get_rate_type_display()}"
return f"Requests #{self.pk}{self.get_rate_type_display()}"
@classmethod
def _baker(cls):

View File

@@ -11,3 +11,5 @@ from .report import * # noqa
from .request import * # noqa
from .valuation import * # noqa
from .vehicle import * # noqa
from .tech_passport import * # noqa
from .certificate import * # noqa

View File

@@ -3,8 +3,9 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers
from core.apps.evaluation.models import AutoEvaluationModel,ReferenceitemModel
from core.apps.evaluation.models import AutoEvaluationModel,ReferenceitemModel, EvaluationrequestModel
from core.apps.evaluation.serializers.reference import ListReferenceitemSerializer
from core.apps.evaluation.choices.request import RequestStatus
User = get_user_model()
@@ -16,11 +17,23 @@ class BaseAutoevaluationSerializer(serializers.ModelSerializer):
value_determined = ListReferenceitemSerializer(read_only=True)
property_rights = ListReferenceitemSerializer(read_only=True)
form_ownership = ListReferenceitemSerializer(read_only=True)
user = serializers.SerializerMethodField(method_name="get_user", read_only=True)
class Meta:
model = AutoEvaluationModel
fields = [
"id",
"contract_date",
"object_inspection_date",
"object_owner_individual_person_passport_num",
"object_owner_type",
"object_owner_individual_person_f_name",
"object_owner_individual_person_l_name",
"object_owner_individual_person_p_name",
"object_owner_legal_entity",
"object_owner_legal_inn",
"tex_passport_serie_num",
"rating_goal",
"registration_number",
"object_type",
"object_type_display",
@@ -36,8 +49,20 @@ class BaseAutoevaluationSerializer(serializers.ModelSerializer):
"rate_type",
"property_rights",
"form_ownership",
"user",
"evaluation_request",
]
def get_user(self, obj):
request = self.context.get('request')
return {
"id": obj.user.id,
"phone": obj.user.phone,
"first_name": obj.user.first_name,
"last_name": obj.user.last_name,
"avatar": request.build_absolute_uri(obj.user.avatar.url) if obj.user.avatar else None
} if obj.user else None
class ListAutoevaluationSerializer(BaseAutoevaluationSerializer):
class Meta(BaseAutoevaluationSerializer.Meta):
@@ -47,12 +72,12 @@ class ListAutoevaluationSerializer(BaseAutoevaluationSerializer):
class RetrieveAutoevaluationSerializer(BaseAutoevaluationSerializer):
car_type_display = serializers.CharField(source="get_car_type_display", read_only=True, default=None)
car_wheel_display = serializers.CharField(source="get_car_wheel_display", read_only=True, default=None)
object_location_highways_display = serializers.CharField(
source="get_object_location_highways_display", read_only=True, default=None
)
object_location_covenience_display = serializers.CharField(
source="get_object_location_covenience_display", read_only=True, default=None
)
# object_location_highways_display = serializers.CharField(
# source="get_object_location_highways_display", read_only=True, default=None
# )
# object_location_covenience_display = serializers.CharField(
# source="get_object_location_covenience_display", read_only=True, default=None
# )
class Meta(BaseAutoevaluationSerializer.Meta):
fields = BaseAutoevaluationSerializer.Meta.fields + [
@@ -71,19 +96,9 @@ class RetrieveAutoevaluationSerializer(BaseAutoevaluationSerializer):
"object_owner_individual_person_passport_num",
"object_owner_legal_entity",
"object_owner_legal_inn",
# Step 3
"object_location_province",
"object_location_district",
"object_location_city",
"object_location_neighborhood",
"object_location_street",
"object_location_home",
"object_location_highways",
"object_location_highways_display",
"object_location_covenience",
"object_location_covenience_display",
# Step 4
"tex_passport_serie_num",
"tex_passport_file",
"tex_passport_gived_date",
"tex_passport_gived_location",
"car_type",
@@ -99,7 +114,7 @@ class RetrieveAutoevaluationSerializer(BaseAutoevaluationSerializer):
]
class CreateAutoevaluationSerializer(serializers.ModelSerializer):
class UpdateAutoevaluationSerializer(serializers.ModelSerializer):
property_rights = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
@@ -150,16 +165,8 @@ class CreateAutoevaluationSerializer(serializers.ModelSerializer):
"form_ownership",
"value_determined",
"rate_type",
# Step 3
"object_location_province",
"object_location_district",
"object_location_city",
"object_location_neighborhood",
"object_location_street",
"object_location_home",
"object_location_highways",
"object_location_covenience",
# Step 4
"tex_passport_file",
"tex_passport_serie_num",
"tex_passport_gived_date",
"tex_passport_gived_location",
@@ -215,10 +222,132 @@ class CreateAutoevaluationSerializer(serializers.ModelSerializer):
return attrs
class CreateAutoevaluationSerializer(serializers.ModelSerializer):
property_rights = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
allow_null=True,
)
value_determined = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
allow_null=True,
)
form_ownership = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
allow_null=True,
)
value_determined = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
allow_null=True,
)
rate_type = serializers.PrimaryKeyRelatedField(
queryset=ReferenceitemModel.objects.all(),
required=False,
allow_null=True,
)
evaluation_request = serializers.PrimaryKeyRelatedField(
queryset=EvaluationrequestModel.objects.all(),
required=False,
allow_null=True,
)
class Meta:
model = AutoEvaluationModel
fields = [
# Step 1
"registration_number",
"evaluation_request",
"contract_date",
"object_inspection_date",
"rate_date",
"rate_report_date",
"rate_object_name",
"object_type",
# Step 2
"object_owner_type",
"object_owner_individual_person_f_name",
"object_owner_individual_person_l_name",
"object_owner_individual_person_p_name",
"object_owner_individual_person_passport_num",
"object_owner_legal_entity",
"object_owner_legal_inn",
"property_rights",
"form_ownership",
"value_determined",
"rate_type",
# Step 4
"tex_passport_serie_num",
"tex_passport_file",
"tex_passport_gived_date",
"tex_passport_gived_location",
"car_type",
"car_wheel",
"car_brand",
"car_model",
"car_number",
"manufacture_year",
"car_dvigatel_number",
"car_color",
]
def validate_tex_passport_serie_num(self, value):
if value and not re.match(r"^[A-Z]{3}\s?\d{7}$", value):
raise serializers.ValidationError(
"Format: AAA 1234567 (3 harf + 7 raqam)"
)
return value
def validate_object_owner_individual_person_passport_num(self, value):
if value and not re.match(r"^[A-Z]{2}\s?\d{7}$", value):
raise serializers.ValidationError(
"Format: AA 1234567 (2 harf + 7 raqam)"
)
return value
def validate(self, attrs):
owner_type = attrs.get("object_owner_type")
if owner_type == 1:
required_fields = {
"object_owner_individual_person_f_name": "Ismi",
"object_owner_individual_person_l_name": "Familiyasi",
"object_owner_individual_person_p_name": "Sharifi",
"object_owner_individual_person_passport_num": "Passport raqami",
}
for field, label in required_fields.items():
if not attrs.get(field):
raise serializers.ValidationError(
{field: f"Jismoniy shaxs uchun {label} majburiy."}
)
elif owner_type == 2:
if not attrs.get("object_owner_legal_entity"):
raise serializers.ValidationError(
{"object_owner_legal_entity": "Yuridik shaxs nomi majburiy."}
)
if not attrs.get("object_owner_legal_inn"):
raise serializers.ValidationError(
{"object_owner_legal_inn": "INN raqami majburiy."}
)
return attrs
def create(self, validated_data):
user = self.context.get('request').user
validated_data['user'] = user
evaluation_req = validated_data.get("evaluation_request")
if evaluation_req:
evaluation_req.status = RequestStatus.IN_PROGRESS
evaluation_req.save()
return super().create(validated_data)
class AutoEvaluationAppraisersSerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.IntegerField())
def validate(self, data):
if not data.get("ids"):
raise serializers.ValidationError("Appraisers IDs are required.")

View File

@@ -0,0 +1 @@
from .certificate import * # noqa

View File

@@ -0,0 +1,13 @@
from rest_framework import serializers
from core.apps.evaluation.models import CertificateModel
class BaseCertificateSerializer(serializers.ModelSerializer):
class Meta:
model = CertificateModel
fields = [
"id",
"title",
"file",
]

View File

@@ -37,6 +37,9 @@ class BaseQuickevaluationSerializer(serializers.ModelSerializer):
"state_car",
"state_car_name",
"created_at",
"distance_covered",
"tex_passport_serie_num",
"is_archive"
]
@@ -51,7 +54,6 @@ class RetrieveQuickevaluationSerializer(BaseQuickevaluationSerializer):
"tex_passport_serie_num",
"tech_passport_issued_date",
"tech_passport_issued_place",
"tex_passport_file",
"car_position",
"car_position_name",
"distance_covered",
@@ -74,7 +76,6 @@ class CreateQuickevaluationSerializer(serializers.ModelSerializer):
"tex_passport_serie_num",
"tech_passport_issued_date",
"tech_passport_issued_place",
"tex_passport_file",
"car_type",
"brand",
"marka",
@@ -125,3 +126,8 @@ class CreateQuickevaluationSerializer(serializers.ModelSerializer):
if request and request.user and request.user.is_authenticated:
validated_data["created_by"] = request.user
return super().create(validated_data)
class ArchiveQuickevaluationSerializer(serializers.Serializer):
id = serializers.IntegerField(required=True)
is_archive = serializers.BooleanField(required=True)

View File

@@ -1,11 +1,16 @@
import re
from django.contrib.auth import get_user_model
from rest_framework import serializers
from core.apps.evaluation.models import EvaluationrequestModel, ReferenceitemModel
from core.apps.evaluation.serializers.reference import ListReferenceitemSerializer
User = get_user_model()
class BaseEvaluationrequestSerializer(serializers.ModelSerializer):
rate_type_display = serializers.CharField(
source="get_rate_type_display", read_only=True
@@ -50,6 +55,7 @@ class BaseEvaluationrequestSerializer(serializers.ModelSerializer):
"user",
"created_at",
"updated_at",
"is_archive",
]
def get_location(self, obj):
@@ -62,13 +68,14 @@ class BaseEvaluationrequestSerializer(serializers.ModelSerializer):
return None
def get_user(self, obj):
request = self.context.get('request')
return {
"id": obj.user.id,
"phone": obj.user.phone,
"first_name": obj.user.first_name,
"last_name": obj.user.last_name,
"role": obj.user.role,
"avatar": obj.user.avatar.url if obj.user.avatar else None
"avatar": request.build_absolute_uri(obj.user.avatar.url) if obj.user.avatar else None
}
@@ -175,6 +182,9 @@ class CreateEvaluationrequestSerializer(serializers.ModelSerializer):
if location_name:
validated_data["location_name"] = str(location_name)
validated_data["user"] = self.context["request"].user
return super().create(validated_data)
class ArchiveEvaluationrequestSerializer(serializers.Serializer):
id = serializers.IntegerField(required=True)
is_archive = serializers.BooleanField(required=True)

View File

@@ -0,0 +1 @@
from .tech_passport import * # noqa

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class TechPassportSerializer(serializers.Serializer):
autonumber = serializers.CharField(required=True, max_length=20)
tech_pass_number = serializers.CharField(required=True, max_length=20)
tech_pass_series = serializers.CharField(required=True, max_length=20)

View File

@@ -26,7 +26,12 @@ from .views import (
AutoEvaluationListAppraisersView,
AutoEvaluationSetAppraisersView,
AutoEvaluationRemoveAppraisersView,
DidoxCompanyInfoAPIView,
TechPassportAPIView,
EvaluationStatusChange,
CertificateView,
ArchiveQuickEvaluationView,
ArchiveEvaluationrequestView,
)
router = DefaultRouter()
@@ -51,6 +56,7 @@ router.register("vehicle", VehicleView, basename="vehicle")
router.register("valuation", ValuationView, basename="valuation")
router.register("property-owner", PropertyOwnerView, basename="property-owner")
router.register("customer", CustomerView, basename="customer")
router.register("certificate", CertificateView, basename="certificate")
urlpatterns = [
path("", include(router.urls)),
path("auto-evaluation/appraisers/", include(
@@ -60,4 +66,18 @@ urlpatterns = [
path("<int:id>/remove/", AutoEvaluationRemoveAppraisersView.as_view(), name="auto-evaluation-remove-appraisers"),
]
)),
path(
"didox/info/<int:tin>/",
DidoxCompanyInfoAPIView.as_view(),
name="didox-info"
),
path(
"tech-passport/",
TechPassportAPIView.as_view(),
name="tech-passport"
),
path("evaluation-request/<int:pk>/change-status/", EvaluationStatusChange.as_view(),
name="evaluation-change-status"),
path("archive/quick-evaluation/", ArchiveQuickEvaluationView.as_view(), name="quick-evaluation-archive"),
path("archive/evaluation-request/", ArchiveEvaluationrequestView.as_view(), name="evaluation-request-archive"),
]

View File

@@ -11,3 +11,6 @@ from .report import * # noqa
from .request import * # noqa
from .valuation import * # noqa
from .vehicle import * # noqa
from .didox import * # noqa
from .tech_passport import * # noqa
from .certificate import * # noqa

View File

@@ -16,7 +16,8 @@ from core.apps.evaluation.serializers.auto import (
CreateAutoevaluationSerializer,
ListAutoevaluationSerializer,
RetrieveAutoevaluationSerializer,
AutoEvaluationAppraisersSerializer
AutoEvaluationAppraisersSerializer,
UpdateAutoevaluationSerializer
)
@@ -39,31 +40,34 @@ class AutoEvaluationView(BaseViewSetMixin, ModelViewSet):
"car_number",
]
ordering_fields = [
"registration_number",
"id",
"contract_date",
"object_inspection_date",
"rate_date",
"rate_report_date",
"rate_object_name",
"object_type",
"object_owner_individual_person_passport_num",
"object_owner_type",
"object_location_province",
"object_location_district",
"object_location_city",
"object_owner_individual_person_f_name",
"object_owner_individual_person_l_name",
"object_owner_individual_person_p_name",
"object_owner_legal_entity",
"object_owner_legal_inn",
"tex_passport_serie_num",
"tex_passport_gived_date",
"rating_goal",
"object_location_province",
"registration_number",
"object_type",
"object_type_display",
"car_brand",
"car_model",
"car_number",
"manufacture_year",
"car_color",
"property_rights",
"form_ownership",
"status",
"status_display",
"created_at",
"value_determined",
"rate_type",
"status",
"created_at",
"updated_at",
"property_rights",
"form_ownership",
]
ordering = ["-created_at"]
@@ -72,8 +76,13 @@ class AutoEvaluationView(BaseViewSetMixin, ModelViewSet):
"list": ListAutoevaluationSerializer,
"retrieve": RetrieveAutoevaluationSerializer,
"create": CreateAutoevaluationSerializer,
"update": UpdateAutoevaluationSerializer,
"partial_update": UpdateAutoevaluationSerializer,
}
def serializer_context(self):
return self.serializer_class(context={'request': self.request})
@extend_schema(tags=["AutoEvaluation"])
class AutoEvaluationSetAppraisersView(GenericAPIView):
@@ -136,8 +145,8 @@ class AutoEvaluationListAppraisersView(GenericAPIView):
query = auto_evaluation.appraisers.all()
if search_query:
query = query.filter(
Q(phone__icontains=search_query) |
Q(first_name__icontains=search_query) |
Q(phone__icontains=search_query) |
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query)
)
page = self.paginate_queryset(query)

View File

@@ -0,0 +1,23 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from core.apps.evaluation.models import CertificateModel
from core.apps.evaluation.serializers.certificate import BaseCertificateSerializer
from rest_framework.filters import SearchFilter
from rest_framework.parsers import MultiPartParser, FormParser
@extend_schema(tags=["Certificate"],request=BaseCertificateSerializer)
class CertificateView(BaseViewSetMixin, ModelViewSet):
queryset = CertificateModel.objects.all()
serializer_class = BaseCertificateSerializer
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
filter_backends = [SearchFilter]
search_fields = ["title"]
pagination_class = None
action_permission_classes = {}

View File

@@ -0,0 +1,60 @@
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.generics import GenericAPIView
from drf_spectacular.utils import extend_schema, OpenApiParameter
from core.services.didox import DidoxService
class DidoxCompanyInfoAPIView(GenericAPIView):
authentication_classes = []
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Didox"],
summary="Get company info by TIN",
description="TIN/JSHSHIR orqali Didoxdan ma'lumot olish",
parameters=[
OpenApiParameter(
name="tin",
type=int,
location=OpenApiParameter.PATH,
required=True,
description="TIN / STIR / INN / JSHSHIR"
)
],
responses={200: dict},
)
def get(self, request, *args, **kwargs):
tin = kwargs.get("tin")
try:
tin = int(tin)
except (TypeError, ValueError):
return Response(
{"detail": "TIN must be a valid integer"},
status=status.HTTP_400_BAD_REQUEST
)
data = DidoxService.get_company_info(tin)
if not data:
return Response(
{"detail": "Didox service unavailable"},
status=status.HTTP_502_BAD_GATEWAY
)
# if both name and personalNum are null/empty -> 404
name = data.get("name")
personal_num = data.get("personalNum")
if not name and not personal_num:
return Response(
{"detail": "Company or person not found"},
status=status.HTTP_404_NOT_FOUND
)
return Response(data, status=status.HTTP_200_OK)

View File

@@ -43,7 +43,14 @@ class AutoEvaluationHistoryView(BaseViewSetMixin, ReadOnlyModelViewSet):
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_class = AutoevaluationhistoryFilter
ordering_fields = ["created_at"]
ordering_fields = [
"id",
"event_type",
"event_type_display",
"actor",
"meta",
"created_at",
]
ordering = ["created_at"]
action_permission_classes = {}

View File

@@ -1,10 +1,14 @@
from django_core.mixins import BaseViewSetMixin
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
from core.apps.evaluation.filters.quick import QuickevaluationFilter
from core.apps.evaluation.models import QuickEvaluationModel
@@ -12,6 +16,7 @@ from core.apps.evaluation.serializers.quick import (
CreateQuickevaluationSerializer,
ListQuickevaluationSerializer,
RetrieveQuickevaluationSerializer,
ArchiveQuickevaluationSerializer,
)
@@ -20,7 +25,7 @@ class QuickEvaluationView(BaseViewSetMixin, ModelViewSet):
queryset = QuickEvaluationModel.objects.select_related(
"created_by", "brand", "marka", "color", "fuel_type",
"body_type", "state_car", "car_position",
).all()
).filter(is_archive=False)
serializer_class = ListQuickevaluationSerializer
permission_classes = [AllowAny]
parser_classes = [MultiPartParser, FormParser]
@@ -50,3 +55,76 @@ class QuickEvaluationView(BaseViewSetMixin, ModelViewSet):
"retrieve": RetrieveQuickevaluationSerializer,
"create": CreateQuickevaluationSerializer,
}
@extend_schema(tags=["QuickEvaluation"])
class ArchiveQuickEvaluationView(GenericAPIView):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if self.request.method == "GET":
return ListQuickevaluationSerializer
return ArchiveQuickevaluationSerializer
@extend_schema(
tags=["QuickEvaluation"],
summary="Get archived quick evaluations list",
description="""
Returns only archived quick evaluations.
This endpoint works like quick-evaluation/,
but only records with is_archive=True are returned.
""",
responses={200: ListQuickevaluationSerializer(many=True)},
)
def get(self, request, *args, **kwargs):
queryset = QuickEvaluationModel.objects.filter(
is_archive=True
).order_by("-created_at")
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
tags=["QuickEvaluation"],
summary="Archive or unarchive quick evaluation",
description="""
Update archive status for quick evaluation.
- is_archive=true → archive
- is_archive=false → remove from archive
""",
request=ArchiveQuickevaluationSerializer,
responses={
200: OpenApiResponse(
description="Archive status updated successfully"
),
400: OpenApiResponse(
description="Validation error"
),
404: OpenApiResponse(
description="Quick evaluation not found"
),
},
)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
obj = get_object_or_404(
QuickEvaluationModel,
id=validated_data["id"]
)
obj.is_archive = validated_data["is_archive"]
obj.save(update_fields=["is_archive"])
return Response(
{
"success": True,
"message": "Archive status updated successfully"
},
status=status.HTTP_200_OK
)

View File

@@ -1,3 +1,5 @@
from django.shortcuts import get_object_or_404
from django_core.mixins import BaseViewSetMixin
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
@@ -5,6 +7,9 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from core.apps.evaluation.filters.request import EvaluationrequestFilter
from core.apps.evaluation.models import EvaluationrequestModel
@@ -12,7 +17,11 @@ from core.apps.evaluation.serializers.request import (
CreateEvaluationrequestSerializer,
ListEvaluationrequestSerializer,
RetrieveEvaluationrequestSerializer,
ArchiveEvaluationrequestSerializer,
)
from core.apps.evaluation.choices.request import RequestStatus
from rest_framework.generics import GenericAPIView
from drf_spectacular.utils import OpenApiResponse
# class RequestPagination(PageNumberPagination):
@@ -35,11 +44,28 @@ class EvaluationrequestView(BaseViewSetMixin, ModelViewSet):
"tex_passport",
]
ordering_fields = [
"id",
"rate_type",
"rate_type_display",
"object_type",
"object_type_display",
"customer_inn_number",
"owner_inn_number",
"tex_passport",
"value_determined",
"rate_goal",
"property_rights",
"form_ownership",
"worked_hours",
"chassi",
"need_delivering",
"location",
"location_name",
"status",
"status_display",
"user",
"created_at",
"updated_at",
"rate_type",
"object_type",
"status",
]
ordering = ["-created_at"]
@@ -50,6 +76,9 @@ class EvaluationrequestView(BaseViewSetMixin, ModelViewSet):
"create": CreateEvaluationrequestSerializer,
}
def serializer_context(self):
return self.serializer_class(context={"request": self.request})
def get_queryset(self):
return EvaluationrequestModel.objects.filter(
user=self.request.user
@@ -71,11 +100,28 @@ class AdminEvaluationrequestView(BaseViewSetMixin, ModelViewSet):
"tex_passport",
]
ordering_fields = [
"id",
"rate_type",
"rate_type_display",
"object_type",
"object_type_display",
"customer_inn_number",
"owner_inn_number",
"tex_passport",
"value_determined",
"rate_goal",
"property_rights",
"form_ownership",
"worked_hours",
"chassi",
"need_delivering",
"location",
"location_name",
"status",
"status_display",
"user",
"created_at",
"updated_at",
"rate_type",
"object_type",
"status",
]
ordering = ["-created_at"]
@@ -88,3 +134,117 @@ class AdminEvaluationrequestView(BaseViewSetMixin, ModelViewSet):
def get_queryset(self):
return EvaluationrequestModel.objects.select_related("value_determined", "rate_goal", "property_rights", "form_ownership", "user").order_by("-created_at")
def serializer_context(self):
return self.serializer_class(context={"request": self.request})
@extend_schema(tags=["EvaluationRequest"])
class EvaluationStatusChange(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, pk):
if request.user.role not in [RoleChoice.ADMIN, RoleChoice.SUPERUSER]:
return Response({'detail': 'Forbidden'}, status=status.HTTP_403_FORBIDDEN)
evaluation = get_object_or_404(EvaluationrequestModel, pk=pk)
status_value = request.data.get('status')
if not status_value:
return Response({'detail': 'Status is required'}, status=status.HTTP_400_BAD_REQUEST)
valid_statuses = [
RequestStatus.PENDING,
RequestStatus.IN_PROGRESS,
RequestStatus.COMPLETED,
RequestStatus.REJECTED
]
if status_value not in valid_statuses:
return Response(
{'detail': f'Invalid status. Must be one of: {valid_statuses}'},
status=status.HTTP_400_BAD_REQUEST
)
evaluation.status = status_value
evaluation.save()
return Response({
'success': True,
'status': evaluation.status,
'id': evaluation.pk
})
@extend_schema(tags=["EvaluationRequest"])
class ArchiveEvaluationrequestView(GenericAPIView):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
if self.request.method == "GET":
return ListEvaluationrequestSerializer
return ArchiveEvaluationrequestSerializer
@extend_schema(
tags=["EvaluationRequest"],
summary="Get archived evaluation requests list",
description="""
Returns only archived evaluation requests.
This endpoint works like evaluation-request/,
but only records with is_archive=True are returned.
""",
responses={200: ListEvaluationrequestSerializer(many=True)},
)
def get(self, request, *args, **kwargs):
queryset = EvaluationrequestModel.objects.filter(
is_archive=True
).order_by("-created_at")
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
tags=["EvaluationRequest"],
summary="Archive or unarchive evaluation request",
description="""
Update archive status for evaluation request.
- is_archive=true → archive
- is_archive=false → remove from archive
""",
request=ArchiveEvaluationrequestSerializer,
responses={
200: OpenApiResponse(
description="Archive status updated successfully"
),
400: OpenApiResponse(
description="Validation error"
),
404: OpenApiResponse(
description="Evaluation request not found"
),
},
)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
obj = get_object_or_404(
EvaluationrequestModel,
id=validated_data["id"]
)
obj.is_archive = validated_data["is_archive"]
obj.save(update_fields=["is_archive"])
return Response(
{
"success": True,
"message": "Archive status updated successfully"
},
status=status.HTTP_200_OK
)

View File

@@ -0,0 +1,59 @@
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.generics import GenericAPIView
from drf_spectacular.utils import (
extend_schema,
OpenApiExample,
)
from core.services.tech_passport import TechPassportService
from ..serializers import TechPassportSerializer
class TechPassportAPIView(GenericAPIView):
authentication_classes = []
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Tech Passport"],
summary="Get vehicle information by technical passport",
description=(
"This endpoint retrieves vehicle information using "
"autonumber, technical passport number, and technical passport series "
"via Gross Insurance API integration."
),
request=TechPassportSerializer,
responses={
200: dict,
400: dict,
},
)
def post(self, request, *args, **kwargs):
serializer = TechPassportSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
result = TechPassportService.get_auto_info(
autonumber=data["autonumber"],
tech_pass_number=data["tech_pass_number"],
tech_pass_series=data["tech_pass_series"],
)
response_data = result["data"]
status_code = result["status_code"]
# success bolsa faqat data ichidagi data qaytariladi
if status_code == 200:
return Response(
response_data.get("data", {}),
status=status.HTTP_200_OK
)
# error bolsa original response qaytariladi
return Response(
response_data,
status=status_code
)

View File

@@ -1 +1,2 @@
from .settings import * # noqa
from .settings import * # noqa
from .region import * # noqa

View File

@@ -0,0 +1,21 @@
from django.contrib import admin
from core.apps.shared.models import RegionModel, DistrictModel, VillageModel
@admin.register(RegionModel)
class RegionAdmin(admin.ModelAdmin):
list_display = ('id', 'name',)
search_fields = ('name',)
@admin.register(DistrictModel)
class DistrictAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'region')
search_fields = ('name', 'region')
@admin.register(VillageModel)
class VillageAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'district')
search_fields = ('name', 'district')

View File

@@ -0,0 +1,140 @@
import json
import os
from django.core.management.base import BaseCommand
from django.db import transaction
from core.apps.shared.models import RegionModel, DistrictModel, VillageModel
class Command(BaseCommand):
help = "Import regions, districts, and neighborhoods from JSON files."
def add_arguments(self, parser):
parser.add_argument('--path', type=str, default='JSON/', help='Path to JSON files directory')
def transliterate_uz_to_en(self, text):
"""Simple transliteration for English names"""
if not text:
return ""
mapping = {
"o'": "o", "O'": "O",
"g'": "g", "G'": "G",
"sh": "sh", "Sh": "Sh",
"ch": "ch", "Ch": "Ch",
"ng": "ng", "Ng": "Ng",
"q": "q", "Q": "Q",
"yo": "yo", "Yo": "Yo",
"yu": "yu", "Yu": "Yu",
"ya": "ya", "Ya": "Ya",
"ye": "ye", "Ye": "Ye",
"o": "o", "O": "O",
"g": "g", "G": "G",
}
res = text
for k, v in mapping.items():
res = res.replace(k, v)
replacements = {
"viloyati": "Region",
"viloyat": "Region",
"tumani": "District",
"tuman": "District",
"shahri": "City",
"shahar": "City",
"Respublikasi": "Republic",
}
for k, v in replacements.items():
res = res.replace(k, v)
res = res.replace(k.capitalize(), v)
return res.strip()
def handle(self, *args, **options):
path = options['path']
regions_file = os.path.join(path, 'region.json')
districts_file = os.path.join(path, 'district.json')
villages_file = os.path.join(path, 'village.json')
try:
with transaction.atomic():
# 1. Import Regions
self.stdout.write("Importing Regions...")
if os.path.exists(regions_file):
with open(regions_file, 'r', encoding='utf-8-sig') as f:
regions_data = json.load(f)
for item in regions_data:
name_uz = item.get('name_uz', '')
# name_ru = item.get('name_ru', '') or name_uz
# name_en = self.transliterate_uz_to_en(name_uz)
# Use get_or_create to avoid duplicates if ID matches or name matches
# Actually, it's better to use our own internal IDs if we want to link them
# But since many JSON files use these IDs, we'll store them as a reference or use them directly if we reset the DB
region, created = RegionModel.objects.update_or_create(
id=item['id'],
defaults={
'name': name_uz,
# 'name_ru': name_ru,
# 'name_en': name_en,
}
)
# 2. Import Districts
self.stdout.write("Importing Districts...")
if os.path.exists(districts_file):
with open(districts_file, 'r', encoding='utf-8-sig') as f:
districts_data = json.load(f)
for item in districts_data:
name = item.get('name_uz', '')
region_id = item.get('region_id')
if not RegionModel.objects.filter(id=region_id).exists():
self.stdout.write(self.style.WARNING(f"Region {region_id} not found for district {name_uz}"))
continue
DistrictModel.objects.update_or_create(
id=item['id'],
defaults={
'region_id': region_id,
'name': name,
# 'name_ru': name_ru,
# 'name_en': name_en,
}
)
# 3. Import Neighborhoods (Villages)
self.stdout.write("Importing Neighborhoods (Villages)...")
if os.path.exists(villages_file):
with open(villages_file, 'r', encoding='utf-8-sig') as f:
villages_data = json.load(f)
count = 0
total = len(villages_data)
for item in villages_data:
name_uz = item.get('name_uz', '')
name_ru = item.get('name_ru', '') or name_uz
name_en = self.transliterate_uz_to_en(name_uz)
district_id = item.get('district_id')
if not DistrictModel.objects.filter(id=district_id).exists():
continue
VillageModel.objects.update_or_create(
id=item['id'],
defaults={
'district_id': district_id,
'name': name_uz,
}
)
count += 1
if count % 1000 == 0:
self.stdout.write(f"Processed {count}/{total} neighborhoods...")
self.stdout.write(self.style.SUCCESS("Import completed successfully!"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error: {str(e)}"))

View File

@@ -0,0 +1,55 @@
# Generated by Django 5.2.7 on 2026-04-20 10:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shared', '0002_settingsmodel_created_at_settingsmodel_description_and_more'),
]
operations = [
migrations.CreateModel(
name='RegionModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255, unique=True)),
],
options={
'verbose_name': 'Region',
'verbose_name_plural': 'Regions',
},
),
migrations.CreateModel(
name='DistrictModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255, unique=True)),
('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='districts', to='shared.regionmodel')),
],
options={
'verbose_name': 'District',
'verbose_name_plural': 'Districts',
},
),
migrations.CreateModel(
name='VillageModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255, unique=True)),
('district', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='villages', to='shared.districtmodel')),
],
options={
'verbose_name': 'Village',
'verbose_name_plural': 'Villages',
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2026-04-21 09:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shared', '0003_regionmodel_districtmodel_villagemodel'),
]
operations = [
migrations.AlterField(
model_name='districtmodel',
name='name',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='regionmodel',
name='name',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='villagemodel',
name='name',
field=models.CharField(max_length=255),
),
]

View File

@@ -1 +1,2 @@
from .settings import * # noqa
from .settings import * # noqa
from .region import * # noqa

View File

@@ -0,0 +1,28 @@
from django.db import models
from django_core.models import AbstractBaseModel
class RegionModel(AbstractBaseModel):
name = models.CharField(max_length=255)
class Meta:
verbose_name = "Region"
verbose_name_plural = "Regions"
class DistrictModel(AbstractBaseModel):
name = models.CharField(max_length=255)
region = models.ForeignKey(RegionModel, on_delete=models.CASCADE, related_name='districts')
class Meta:
verbose_name = "District"
verbose_name_plural = "Districts"
class VillageModel(AbstractBaseModel):
name = models.CharField(max_length=255)
district = models.ForeignKey(DistrictModel, on_delete=models.CASCADE, related_name='villages')
class Meta:
verbose_name = "Village"
verbose_name_plural = "Villages"

View File

@@ -0,0 +1,21 @@
from rest_framework import serializers
from core.apps.shared.models import RegionModel, VillageModel, DistrictModel
class DistrictSerializer(serializers.ModelSerializer):
class Meta:
model = DistrictModel
fields = ('id', 'name')
class VillageSerializer(serializers.ModelSerializer):
class Meta:
model = VillageModel
fields = ('id', 'name')
class RegionSerializer(serializers.ModelSerializer):
class Meta:
model = RegionModel
fields = ('id', 'name')

View File

@@ -1,6 +1,7 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import SettingsView
from .views.region.region import RegionListCreateView, DistrictListCreateView, VillageListCreateView
router = DefaultRouter()
router.register("settings", SettingsView, basename="settings")
@@ -8,4 +9,7 @@ router.register("settings", SettingsView, basename="settings")
urlpatterns = [
path("", include(router.urls)),
path("region/", RegionListCreateView.as_view(), name="region-list-create"),
path("district/", DistrictListCreateView.as_view(), name="district-list-create"),
path("village/", VillageListCreateView.as_view(), name="village-list-create"),
]

View File

@@ -0,0 +1,74 @@
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
from core.apps.shared.serializers.region.district import DistrictSerializer, VillageSerializer, RegionSerializer
from core.apps.shared.models import RegionModel, VillageModel, DistrictModel
@extend_schema(
parameters=[
OpenApiParameter(
name="region",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
description="Filter by region ID"
),
OpenApiParameter(
name="name",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by district name"
),
]
)
class DistrictListCreateView(generics.ListCreateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = DistrictSerializer
queryset = DistrictModel.objects.all()
def get_queryset(self):
region = self.request.query_params.get('region')
name = self.request.query_params.get('name')
if name:
return self.queryset.filter(name__icontains=name)
if region:
return self.queryset.filter(region=region)
return super().get_queryset()
@extend_schema(
parameters=[
OpenApiParameter(
name="district",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
description="Filter by district ID"
),
OpenApiParameter(
name="name",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Filter by village name"
),
]
)
class VillageListCreateView(generics.ListCreateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = VillageSerializer
queryset = VillageModel.objects.all()
def get_queryset(self):
district = self.request.query_params.get('district')
name = self.request.query_params.get('name')
if district:
return self.queryset.filter(district=district)
if name:
return self.queryset.filter(name__icontains=name)
return super().get_queryset()
class RegionListCreateView(generics.ListCreateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = RegionSerializer
queryset = RegionModel.objects.all()

View File

@@ -1,3 +1,5 @@
from .otp import * # noqa
from .sms import * # noqa
from .user import * # noqa
from .didox import * # noqa
from .tech_passport import * # noqa

27
core/services/didox.py Normal file
View File

@@ -0,0 +1,27 @@
import requests
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class DidoxService:
BASE_URL = "https://testapi3.didox.uz/v1"
@classmethod
def get_company_info(cls, tin: int) -> dict:
url = f"{cls.BASE_URL}/utils/info/{tin}"
headers = {"Partner-Authorization": settings.DIDOX_PARTNER_TOKEN}
try:
response = requests.get(url=url, headers=headers, timeout=15,verify=False)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
logger.exception(f"Didox API error: {str(e)}")
return {}

View File

@@ -0,0 +1,71 @@
import requests
import logging
logger = logging.getLogger(__name__)
class TechPassportService:
BASE_URL = "https://api-test.gross.uz/api/v1/osago/check-tech-data"
@classmethod
def get_auto_info(
cls,
autonumber: str,
tech_pass_number: str,
tech_pass_series: str
):
payload = {
"tech_data": {
"autonumber": autonumber,
"tech_pass_number": tech_pass_number,
"tech_pass_series": tech_pass_series,
},
"payload": {
"promo": "",
"autotype": 1,
"citizen": 1,
"number": 1,
"period": 1,
"region": 1,
"coeff": 1
}
}
headers = {
"Content-Type": "application/json"
}
try:
response = requests.post(
cls.BASE_URL,
json=payload,
headers=headers,
timeout=30,
verify=False
)
logger.info(
f"Tech passport response status: {response.status_code}"
)
try:
response_data = response.json()
except ValueError:
response_data = {
"detail": "Invalid response from external service"
}
return {
"status_code": response.status_code,
"data": response_data
}
except requests.exceptions.RequestException as e:
logger.error(str(e))
return {
"status_code": 500,
"data": {
"detail": str(e)
}
}

View File

@@ -56,3 +56,20 @@ services:
image: redis
celery:
build:
context: .
dockerfile: ./docker/Dockerfile.web
command: celery -A config worker --loglevel=info
volumes:
- "./:/code"
depends_on:
- redis
- web
networks:
- sifatbaho
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
restart: always

1682
resources/json/district.json Normal file

File diff suppressed because it is too large Load Diff

100
resources/json/region.json Normal file
View File

@@ -0,0 +1,100 @@
[
{
"id": 2,
"soato_id": 1703,
"name_uz": "Andijon viloyati",
"name_oz": "Андижон вилояти",
"name_ru": "Андижанская область"
},
{
"id": 3,
"soato_id": 1706,
"name_uz": "Buxoro viloyati",
"name_oz": "Бухоро вилояти",
"name_ru": "Бухарская область"
},
{
"id": 4,
"soato_id": 1708,
"name_uz": "Jizzax viloyati",
"name_oz": "Жиззах вилояти",
"name_ru": "Джизакская область"
},
{
"id": 5,
"soato_id": 1710,
"name_uz": "Qashqadaryo viloyati",
"name_oz": "Қашқадарё вилояти",
"name_ru": "Кашкадарьинская область"
},
{
"id": 6,
"soato_id": 1712,
"name_uz": "Navoiy viloyati",
"name_oz": "Навоий вилояти",
"name_ru": "Навоийская область"
},
{
"id": 7,
"soato_id": 1714,
"name_uz": "Namangan viloyati",
"name_oz": "Наманган вилояти",
"name_ru": "Наманганская область"
},
{
"id": 8,
"soato_id": 1718,
"name_uz": "Samarqand viloyati",
"name_oz": "Самарқанд вилояти",
"name_ru": "Самаркандская область"
},
{
"id": 10,
"soato_id": 1724,
"name_uz": "Sirdaryo viloyati",
"name_oz": "Сирдарё вилояти",
"name_ru": "Сырдарьинская область"
},
{
"id": 11,
"soato_id": 1726,
"name_uz": "Toshkent shahri",
"name_oz": "Тошкент шаҳри",
"name_ru": "город Ташкент"
},
{
"id": 12,
"soato_id": 1727,
"name_uz": "Toshkent viloyati",
"name_oz": "Тошкент вилояти",
"name_ru": "Ташкентская область"
},
{
"id": 13,
"soato_id": 1730,
"name_uz": "Farg'ona viloyati",
"name_oz": "Фарғона вилояти",
"name_ru": "Ферганская область"
},
{
"id": 14,
"soato_id": 1733,
"name_uz": "Xorazm viloyati",
"name_oz": "Хоразм вилояти",
"name_ru": "Хорезмская область"
},
{
"id": 15,
"soato_id": 1735,
"name_uz": "Qoraqalpog'iston Respublikasi",
"name_oz": "Қорақалпоғистон Республикаси",
"name_ru": "Республика Каракалпакстан"
},
{
"id": 5723,
"soato_id": 1722,
"name_uz": "Surxondaryo viloyati",
"name_oz": "Сурхондарё вилояти",
"name_ru": "Сурхандарьинская область"
}
]

21130
resources/json/village.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ services:
max_failure_ratio: 0.2
resources:
limits:
cpus: '1.00'
cpus: "1.00"
memory: 512M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d sifatbaho"]
@@ -69,7 +69,7 @@ services:
max_failure_ratio: 0.2
resources:
limits:
cpus: '0.50'
cpus: "0.50"
memory: 256M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
@@ -84,7 +84,7 @@ services:
max-file: "5"
web:
image: husanjon/sifatbaho:65
image: husanjon/sifatbaho:114
env_file:
- .env
environment:
@@ -114,7 +114,7 @@ services:
max_failure_ratio: 0.2
resources:
limits:
cpus: '1.50'
cpus: "1.50"
memory: 1024M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/health/ || exit 1"]
@@ -128,13 +128,47 @@ services:
max-size: "50m"
max-file: "5"
celery:
image: husanjon/sifatbaho:114
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=config.settings.production
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
command: >
sh -c "
echo 'Waiting for redis...' &&
sleep 5 &&
celery -A config worker --loglevel=info
"
networks:
- sifatbaho
volumes:
- logs:/code/resources/logs/:rw
- pycache:/var/cache/pycache:rw
deploy:
mode: replicated
replicas: 1
restart_policy:
condition: any
update_config:
parallelism: 1
order: start-first
failure_action: rollback
logging:
driver: json-file
options:
max-size: "20m"
max-file: "3"
nginx:
image: nginx:alpine
ports:
- mode: ingress
target: 80
published: ${PORT:?enviromentda PORT topilmadi}
published: ${PORT:?enviromentda PORT topilmadi}
protocol: tcp
volumes:
- type: bind
@@ -161,7 +195,7 @@ services:
max_failure_ratio: 0.2
resources:
limits:
cpus: '0.50'
cpus: "0.50"
memory: 256M
tmpfs:
- /var/cache/nginx